Merge branch 'dev' into feature/design-remake

# Conflicts:
#	src/components/floor-plan/CanvasFrame.jsx
This commit is contained in:
Jaeyoung Lee 2025-09-10 13:19:06 +09:00
commit 098fb8efc6
66 changed files with 3723 additions and 204 deletions

View File

@ -55,11 +55,14 @@
},
"devDependencies": {
"@turf/turf": "^7.0.0",
"@types/node": "^24.3.0",
"@types/react": "^19.1.11",
"convertapi": "^1.14.0",
"postcss": "^8",
"prettier": "^3.3.3",
"react-color-palette": "^7.2.2",
"sass": "^1.77.8",
"tailwindcss": "^3.4.1"
"tailwindcss": "^3.4.1",
"typescript": "^5.9.2"
}
}

View File

@ -13,6 +13,7 @@ export const MENU = {
MOVEMENT_SHAPE_UPDOWN: 'movementShapeUpdown', // 동선이동.형올림내림
OUTLINE_EDIT_OFFSET: 'outlineEditOffset', // 외벽선 편집 및 오프셋
ROOF_SHAPE_ALLOC: 'rootShapeAlloc', // 지붕면 항당
ALL_REMOVE: 'allRemove', // 전체 삭제
DEFAULT: 'roofCoveringDefault', // 아무것도 선택 안할 경우
}, // 지붕덮개
BATCH_CANVAS: {

View File

@ -0,0 +1,434 @@
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)
// 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()
}
}
}
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)
} 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)
}
})
}
//
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.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)
}
})
}
// 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) {
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
}
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('.')) {
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) //
setShowKeypad(false)
}
// : (preventDefault )
break
case 'Backspace':
case 'Delete':
handleDelete()
break
case 'Escape':
handleClear()
setShowKeypad(false)
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'}
/>
{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}
</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>
)
})
CalculatorInput.displayName = 'CalculatorInput'

View File

@ -2,6 +2,7 @@ import { fabric } from 'fabric'
import { v4 as uuidv4 } from 'uuid'
import { getDirectionByPoint } from '@/util/canvas-util'
import { calcLinePlaneSize } from '@/util/qpolygon-utils'
import { logger } from '@/util/logger'
export const QLine = fabric.util.createClass(fabric.Line, {
type: 'QLine',
@ -69,7 +70,14 @@ export const QLine = fabric.util.createClass(fabric.Line, {
},
setLength() {
this.length = calcLinePlaneSize(this) / 10
// Ensure all required properties are valid numbers
const { x1, y1, x2, y2 } = this;
if (isNaN(x1) || isNaN(y1) || isNaN(x2) || isNaN(y2)) {
logger.error('Invalid coordinates in QLine:', { x1, y1, x2, y2 });
this.length = 0;
return;
}
this.length = calcLinePlaneSize({ x1, y1, x2, y2 }) / 10;
},
addLengthText() {

View File

@ -66,12 +66,17 @@ export default function CanvasFrame() {
canvas?.loadFromJSON(JSON.parse(plan.canvasStatus), function () {
canvasLoadInit() //config
canvas?.renderAll() // .
if (canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.MODULE).length > 0) {
setSelectedMenu('module')
} else if (canvas.getObjects().length === 0 || canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.WALL).length > 0) {
setTimeout(() => {
setSelectedMenu('module')
}, 500)
} else if (canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.WALL).length > 0) {
setSelectedMenu('outline')
} else {
setSelectedMenu('surface')
setTimeout(() => {
setSelectedMenu('surface')
}, 500)
}
const roofs = canvas.getObjects().filter((obj) => obj.name === POLYGON_TYPE.ROOF)

View File

@ -169,6 +169,7 @@ const Placement = forwardRef((props, refs) => {
<div className="roof-module-table">
<table>
<thead>
<tr>
{moduleData.header.map((data) => (
<th key={data.prop} style={{ width: data.width ? data.width : '' }}>
{data.type === 'check' ? (
@ -181,6 +182,7 @@ const Placement = forwardRef((props, refs) => {
)}
</th>
))}
</tr>
</thead>
<tbody>
{selectedModules?.itemList &&
@ -216,7 +218,7 @@ const Placement = forwardRef((props, refs) => {
className="input-origin block"
name="row"
value={props.layoutSetup[index]?.row ?? 1}
defaultValue={0}
//defaultValue={0}
onChange={(e) => handleLayoutSetup(e, item.itemId, index)}
/>
</div>
@ -228,7 +230,7 @@ const Placement = forwardRef((props, refs) => {
className="input-origin block"
name="col"
value={props.layoutSetup[index]?.col ?? 1}
defaultValue={0}
//defaultValue={0}
onChange={(e) => handleLayoutSetup(e, item.itemId, index)}
/>
</div>

View File

@ -122,7 +122,7 @@ export default function CircuitTrestleSetting({ id }) {
canvas.set({ zoom: 1 })
// roof
const roofs = canvas.getObjects().filter((obj) => obj.name === 'roof')
const roofs = canvas.getObjects().filter((obj) => obj.name === 'roof' && !obj.wall)
if (roofs.length > 0) {
// roof x, y

View File

@ -66,7 +66,9 @@ export default function Eaves({ pitchRef, offsetRef, widthRef, radioTypeRef, pit
</div>
<div className="eaves-keraba-td">
<div className={`eaves-keraba-ico ${type === '1' ? 'act' : ''}`}>
<Image src="/static/images/canvas/eaves_icon01.svg" alt="react" width={30} height={30} />
<div style={{ width: 30, height: 30, position: 'relative' }}>
<Image src="/static/images/canvas/eaves_icon01.svg" alt="react" fill style={{ objectFit: 'contain' }} />
</div>
</div>
</div>
</div>
@ -79,7 +81,9 @@ export default function Eaves({ pitchRef, offsetRef, widthRef, radioTypeRef, pit
</div>
<div className="eaves-keraba-td">
<div className={`eaves-keraba-ico ${type === '2' ? 'act' : ''}`}>
<Image src="/static/images/canvas/eaves_icon02.svg" alt="react" width={30} height={30} />
<div style={{ width: 30, height: 30, position: 'relative' }}>
<Image src="/static/images/canvas/eaves_icon02.svg" alt="react" fill style={{ objectFit: 'contain' }} />
</div>
</div>
</div>
</div>
@ -105,7 +109,9 @@ export default function Eaves({ pitchRef, offsetRef, widthRef, radioTypeRef, pit
</div>
<div className="eaves-keraba-td">
<div className="eaves-keraba-ico ">
<Image src="/static/images/canvas/eaves_icon03.svg" alt="react" width={30} height={30} />
<div style={{ width: 30, height: 30, position: 'relative' }}>
<Image src="/static/images/canvas/eaves_icon03.svg" alt="react" fill style={{ objectFit: 'contain' }} />
</div>
</div>
</div>
</div>

View File

@ -37,7 +37,9 @@ export default function Gable({ pitchRef, offsetRef, widthRef, radioTypeRef, pit
</div>
<div className="eaves-keraba-td">
<div className={`eaves-keraba-ico ${type === '1' ? 'act' : ''}`}>
<Image src="/static/images/canvas/eaves_icon04.svg" alt="react" width={30} height={30} />
<div style={{ width: 30, height: 30, position: 'relative' }}>
<Image src="/static/images/canvas/eaves_icon04.svg" alt="react" fill style={{ objectFit: 'contain' }} />
</div>
</div>
</div>
</div>
@ -50,7 +52,9 @@ export default function Gable({ pitchRef, offsetRef, widthRef, radioTypeRef, pit
</div>
<div className="eaves-keraba-td">
<div className={`eaves-keraba-ico ${type === '2' ? 'act' : ''}`}>
<Image src="/static/images/canvas/eaves_icon09.svg" alt="react" width={30} height={30} />
<div style={{ width: 30, height: 30, position: 'relative' }}>
<Image src="/static/images/canvas/eaves_icon09.svg" alt="react" fill style={{ objectFit: 'contain' }} />
</div>
</div>
</div>
</div>
@ -74,7 +78,9 @@ export default function Gable({ pitchRef, offsetRef, widthRef, radioTypeRef, pit
</div>
<div className="eaves-keraba-td">
<div className="eaves-keraba-ico ">
<Image src="/static/images/canvas/eaves_icon05.svg" alt="react" width={30} height={30} />
<div style={{ width: 30, height: 30, position: 'relative' }}>
<Image src="/static/images/canvas/eaves_icon05.svg" alt="react" fill style={{ objectFit: 'contain' }} />
</div>
</div>
</div>
</div>
@ -92,7 +98,9 @@ export default function Gable({ pitchRef, offsetRef, widthRef, radioTypeRef, pit
</div>
<div className="eaves-keraba-td">
<div className="eaves-keraba-ico ">
<Image src="/static/images/canvas/eaves_icon10.svg" alt="react" width={30} height={30} />
<div style={{ width: 30, height: 30, position: 'relative' }}>
<Image src="/static/images/canvas/eaves_icon10.svg" alt="react" fill style={{ objectFit: 'contain' }} />
</div>
</div>
</div>
</div>

View File

@ -23,7 +23,9 @@ export default function WallMerge({ offsetRef, radioTypeRef }) {
</div>
<div className="eaves-keraba-td">
<div className={`eaves-keraba-ico ${type === '1' ? 'act' : ''}`}>
<Image src="/static/images/canvas/eaves_icon06.svg" alt="react" width={30} height={30} />
<div style={{ width: 30, height: 30, position: 'relative' }}>
<Image src="/static/images/canvas/eaves_icon06.svg" alt="react" fill style={{ objectFit: 'contain' }} />
</div>
</div>
</div>
</div>
@ -36,7 +38,9 @@ export default function WallMerge({ offsetRef, radioTypeRef }) {
</div>
<div className="eaves-keraba-td">
<div className={`eaves-keraba-ico ${type === '2' ? 'act' : ''}`}>
<Image src="/static/images/canvas/eaves_icon07.svg" alt="react" width={30} height={30} />
<div style={{ width: 30, height: 30, position: 'relative' }}>
<Image src="/static/images/canvas/eaves_icon07.svg" alt="react" fill style={{ objectFit: 'contain' }} />
</div>
</div>
</div>
</div>
@ -54,7 +58,9 @@ export default function WallMerge({ offsetRef, radioTypeRef }) {
</div>
<div className="eaves-keraba-td">
<div className="eaves-keraba-ico ">
<Image src="/static/images/canvas/eaves_icon08.svg" alt="react" width={30} height={30} />
<div style={{ width: 30, height: 30, position: 'relative' }}>
<Image src="/static/images/canvas/eaves_icon08.svg" alt="react" fill style={{ objectFit: 'contain' }} />
</div>
</div>
</div>
</div>

View File

@ -8,6 +8,7 @@ import { usePopup } from '@/hooks/usePopup'
import { canvasState } from '@/store/canvasAtom'
import { usePolygon } from '@/hooks/usePolygon'
import { useSurfaceShapeBatch } from '@/hooks/surface/useSurfaceShapeBatch'
import { useRoofFn } from '@/hooks/common/useRoofFn'
const FLOW_DIRECTION_TYPE = {
EIGHT_AZIMUTH: 'eightAzimuth',
@ -19,6 +20,7 @@ export default function FlowDirectionSetting(props) {
const { id, pos = contextPopupPosition, target } = props
const canvas = useRecoilValue(canvasState)
const { getMessage } = useMessage()
const { setSurfaceShapePattern } = useRoofFn()
const { changeSurfaceLineType } = useSurfaceShapeBatch({})
@ -79,6 +81,7 @@ export default function FlowDirectionSetting(props) {
surfaceCompass: orientation,
surfaceCompassType: type,
})
setSurfaceShapePattern(roof, null, null, roof.roofMaterial)
drawDirectionArrow(roof)
canvas?.renderAll()
changeSurfaceLineType(roof)

View File

@ -2,29 +2,90 @@
import { useMessage } from '@/hooks/useMessage'
import { normalizeDigits } from '@/util/input-utils'
import { getChonByDegree, getDegreeByChon } from '@/util/canvas-util'
import { CalculatorInput } from '@/components/common/input/CalcInput'
import { useEffect, useRef } from 'react'
export default function OuterLineWall({ props }) {
const { getMessage } = useMessage()
const { length1, setLength1, length1Ref, arrow1, setArrow1 } = props
//
useEffect(() => {
const handleKeyDown = (e) => {
//
const keypadVisible = document.querySelector('.keypad-container')
//
if (keypadVisible) {
return
}
// input
if (document.activeElement && document.activeElement.classList.contains('calculator-input')) {
return
}
//
if (e.key === 'Enter') {
// (useOuterLineWall.js )
return
}
// input
if (/^[0-9+\-*\/=.]$/.test(e.key) || e.key === 'Backspace' || e.key === 'Delete') {
const calcInput = document.querySelector('.calculator-input')
if (calcInput) {
// preventDefault
calcInput.focus()
calcInput.click()
}
}
}
// capture: true
document.addEventListener('keydown', handleKeyDown, { capture: true })
return () => document.removeEventListener('keydown', handleKeyDown, { capture: true })
}, [])
return (
<div className="outline-wrap">
<div className="outline-inner">
<div className="outline-form">
<span className="mr10">{getMessage('straight.line')}</span>
<div className="input-grid" style={{ width: '63px' }}>
<input
type="text"
className="input-origin block"
{/*<input*/}
{/* type="text"*/}
{/* className="input-origin block"*/}
{/* value={length1}*/}
{/* ref={length1Ref}*/}
{/* onFocus={(e) => {*/}
{/* if (length1Ref.current.value === '0') {*/}
{/* length1Ref.current.value = ''*/}
{/* }*/}
{/* }}*/}
{/* onChange={(e) => setLength1(normalizeDigits(e.target.value))}*/}
{/* placeholder="3000"*/}
{/*/>*/}
<CalculatorInput
id="length1-calc"
label=""
className="input-origin block calculator-input"
readOnly={false}
value={length1}
onChange={(value) => setLength1(value)}
options={{
allowNegative: false,
allowDecimal: false //(index !== 0),
}}
placeholder={'3000'}
ref={length1Ref}
onFocus={(e) => {
if (length1Ref.current.value === '0') {
onFocus={() => {
if (length1Ref.current && length1Ref.current.value === '0') {
length1Ref.current.value = ''
}
}}
onChange={(e) => setLength1(normalizeDigits(e.target.value))}
placeholder="3000"
/>
</div>
<button className="reset-btn" onClick={() => setLength1(0)}></button>

View File

@ -12,11 +12,12 @@ export default function FlowLine({ FLOW_LINE_REF }) {
const { getMessage } = useMessage()
const [type, setType] = useState(FLOW_LINE_TYPE.DOWN_LEFT)
const [filledInput, setFilledInput] = useState('')
const [pointerInput, setPointerInput] = useState('100')
const currentObject = useRecoilValue(currentObjectState)
const handleFocus = () => {
if (currentObject === null) {
FLOW_LINE_REF.POINTER_INPUT_REF.current.value = ''
FLOW_LINE_REF.FILLED_INPUT_REF.current.value = ''
setPointerInput('')
setFilledInput('')
FLOW_LINE_REF.FILLED_INPUT_REF.current.blur()
}
}
@ -35,7 +36,7 @@ export default function FlowLine({ FLOW_LINE_REF }) {
<div className="outline-form">
<span>{getMessage('modal.movement.flow.line.position')}</span>
<div className="input-grid mr5">
<input type="text" className="input-origin block" defaultValue={100} readOnly={true} ref={FLOW_LINE_REF.POINTER_INPUT_REF} />
<input type="text" className="input-origin block" value={pointerInput} readOnly={true} ref={FLOW_LINE_REF.POINTER_INPUT_REF} />
</div>
</div>
<div className="moving-tab-content">
@ -71,7 +72,6 @@ export default function FlowLine({ FLOW_LINE_REF }) {
<input
type="text"
className="input-origin block"
defaultValue={100}
ref={FLOW_LINE_REF.FILLED_INPUT_REF}
value={filledInput}
onFocus={handleFocus}

View File

@ -21,6 +21,7 @@ import { useRoofFn } from '@/hooks/common/useRoofFn'
import { usePlan } from '@/hooks/usePlan'
import { normalizeDecimal, normalizeDigits } from '@/util/input-utils'
import { logger } from '@/util/logger'
import { CalculatorInput } from '@/components/common/input/CalcInput'
/**
* 지붕 레이아웃
@ -326,11 +327,11 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla
</div>
</span>
<div className="input-grid mr5">
<input
{/* <input
type="text"
className="input-origin block"
readOnly={currentRoof?.roofAngleSet !== item.value}
value={index === 0 ? (currentRoof?.pitch || '0') : (currentRoof?.angle || '0')}
value={index === 0 ? currentRoof?.pitch || '0' : currentRoof?.angle || '0'}
onChange={(e) => {
const v = normalizeDecimal(e.target.value)
e.target.value = v
@ -342,6 +343,34 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla
setCurrentRoof({ ...currentRoof, pitch: num === '' ? '' : getChonByDegree(num), angle: num === '' ? '' : num })
}
}}
/> */}
<CalculatorInput
id=""
label=""
className="input-origin block"
readOnly={currentRoof?.roofAngleSet !== item.value}
value={index === 0 ? currentRoof?.pitch || '0' : currentRoof?.angle || '0'}
onChange={(value) => {
if (index === 0) {
const num = value === '' ? '' : Number(value)
setCurrentRoof(prev => ({
...prev,
pitch: num === '' ? '' : num,
angle: num === '' ? '' : getDegreeByChon(num),
}))
} else {
const num = value === '' ? '' : Number(value)
setCurrentRoof( prev => ({
...prev,
pitch: num === '' ? '' : getChonByDegree(num),
angle: num === '' ? '' : num,
}))
}
}}
options={{
allowNegative: false,
allowDecimal: false //(index !== 0),
}}
/>
</div>
<span className="thin">{index === 0 ? '寸' : '度'}</span>
@ -420,10 +449,8 @@ export default function PlacementShapeSetting({ id, pos = { x: 50, y: 180 }, pla
<div className="select-wrap" style={{ width: '160px' }}>
<QSelectBox
options={raftCodes}
title={
raftCodes?.find((r) => r.clCode === ( currentRoof.raft?? currentRoof?.raftBaseCd))?.clCodeNm
}
value={currentRoof?.raft??currentRoof?.raftBaseCd}
title={raftCodes?.find((r) => r.clCode === (currentRoof.raft ?? currentRoof?.raftBaseCd))?.clCodeNm}
value={currentRoof?.raft ?? currentRoof?.raftBaseCd}
onChange={(e) => handleRafterChange(e.clCode)}
sourceKey="clCode"
targetKey={currentRoof?.raft ? 'raft' : 'raftBaseCd'}

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 && 'checked'} readOnly={true} />
<input type="radio" name="radio01" checked={roof.selected} readOnly />
<label
htmlFor="ra01"
onClick={(e) => {
@ -213,7 +213,6 @@ export default function RoofAllocationSetting(props) {
handleChangePitch(e, index)
}}
value={currentAngleType === 'slope' ? roof.pitch : roof.angle}
defaultValue={currentAngleType === 'slope' ? roof.pitch : roof.angle}
/>
</div>
<span className="absol">{pitchText}</span>

View File

@ -66,6 +66,9 @@ export default function useMenu() {
case MENU.ROOF_COVERING.ROOF_SHAPE_ALLOC:
addPopup(popupId, 1, <RoofAllocationSetting id={popupId} />)
break
case MENU.ROOF_COVERING.ALL_REMOVE:
deleteAllSurfacesAndObjects()
break
}
}

View File

@ -6,6 +6,8 @@ import { POLYGON_TYPE } from '@/common/common'
import { useEvent } from '@/hooks/useEvent'
import { useLine } from '@/hooks/useLine'
import { outerLinePointsState } from '@/store/outerLineAtom'
import { usePolygon } from '@/hooks/usePolygon'
import { useText } from '@/hooks/useText'
const ROOF_COLOR = {
0: 'rgb(199,240,213)',
@ -13,6 +15,7 @@ const ROOF_COLOR = {
2: 'rgb(187,204,255)',
3: 'rgb(228,202,255)',
}
export function useRoofFn() {
const canvas = useRecoilValue(canvasState)
const selectedRoofMaterial = useRecoilValue(selectedRoofMaterialSelector)
@ -20,6 +23,8 @@ export function useRoofFn() {
const { addCanvasMouseEventListener, initEvent } = useEvent()
const resetPoints = useResetRecoilState(outerLinePointsState)
const { addPitchText } = useLine()
const { setPolygonLinesActualSize } = usePolygon()
const { changeCorridorDimensionText } = useText()
//면형상 선택 클릭시 지붕 패턴 입히기
function setSurfaceShapePattern(polygon, mode = 'onlyBorder', trestleMode = false, roofMaterial, isForceChange = false, isDisplay = false) {
@ -27,6 +32,9 @@ export function useRoofFn() {
if (!polygon) {
return
}
if (polygon.wall) {
return
}
if (polygon.points.length < 3) {
return
}
@ -44,6 +52,7 @@ export function useRoofFn() {
let width = (roofMaterial.width || 226) / 10
let height = (roofMaterial.length || 158) / 10
const index = roofMaterial.index ?? 0
let roofStyle = 2
const inputPatternSize = { width: width, height: height } //임시 사이즈
@ -169,6 +178,8 @@ export function useRoofFn() {
polygon.set('fill', null)
polygon.set('fill', pattern)
polygon.roofMaterial = roofMaterial
setPolygonLinesActualSize(polygon)
changeCorridorDimensionText()
polygon.canvas?.renderAll()
} catch (e) {
console.log(e)
@ -303,7 +314,15 @@ export function useRoofFn() {
}
function convertAbsolutePoint(area) {
return area.points.map((p) => fabric.util.transformPoint({ x: p.x - area.pathOffset.x, y: p.y - area.pathOffset.y }, area.calcTransformMatrix()))
return area.points.map((p) =>
fabric.util.transformPoint(
{
x: p.x - area.pathOffset.x,
y: p.y - area.pathOffset.y,
},
area.calcTransformMatrix(),
),
)
}
const removeOuterLines = (currentMousePos) => {

View File

@ -63,6 +63,7 @@ export function useModuleBasicSetting(tabNum) {
const { checkModuleDisjointSurface } = useTurf()
useEffect(() => {
initEvent()
return () => {
//수동 설치시 초기화
removeMouseEvent('mouse:up')
@ -139,7 +140,7 @@ export function useModuleBasicSetting(tabNum) {
roof.lines.forEach((line) => {
line.attributes = {
...line.attributes,
offset: getOffset(offsetObjects.addRoof, line, roof.pitch, roof.from),
offset: getOffset(offsetObjects.addRoof, line, roof.roofMaterial.pitch),
}
})
//배치면 설치 영역
@ -209,9 +210,9 @@ export function useModuleBasicSetting(tabNum) {
const calculateHeightRate = 1 / Math.cos((degree * Math.PI) / 180)
const calculateValue = calculateHeightRate / calculateExpression(degree)
const eavesResult = from === 'roofCover' ? (data.eavesMargin * Math.cos((degree * Math.PI) / 180)) / 10 : data.eavesMargin / 10
const ridgeResult = from === 'roofCover' ? (data.ridgeMargin * Math.cos((degree * Math.PI) / 180)) / 10 : data.ridgeMargin / 10
const kerabaMargin = from === 'roofCover' && isDiagonal ? data.kerabaMargin / calculateValue / 10 : data.kerabaMargin / 10
const eavesResult = +roofSizeSet === 1 ? (data.eavesMargin * Math.cos((degree * Math.PI) / 180)) / 10 : data.eavesMargin / 10
const ridgeResult = +roofSizeSet === 1 ? (data.ridgeMargin * Math.cos((degree * Math.PI) / 180)) / 10 : data.ridgeMargin / 10
const kerabaMargin = +roofSizeSet === 1 && isDiagonal ? data.kerabaMargin / calculateValue / 10 : data.kerabaMargin / 10
switch (line.attributes.type) {
case LINE_TYPE.WALLLINE.EAVES:
@ -232,7 +233,7 @@ export function useModuleBasicSetting(tabNum) {
//가대 상세 데이터 기준으로 모듈 설치 배치면 생성
const makeModuleInstArea = (roof, trestleDetail) => {
//지붕 객체 반환
if (tabNum == 3) {
if (!roof) {
return
@ -556,7 +557,7 @@ export function useModuleBasicSetting(tabNum) {
let tmpHeight = flowDirection === 'south' || flowDirection === 'north' ? moduleHeight : moduleWidth
let { width, height } =
moduleSetupSurface.from === 'roofCover'
+roofSizeSet === 1
? calculateVisibleModuleHeight(tmpWidth, tmpHeight, getDegreeByChon(moduleSetupSurfaces[i].roofMaterial.pitch), flowDirection)
: { width: tmpWidth, height: tmpHeight }
@ -1056,11 +1057,11 @@ export function useModuleBasicSetting(tabNum) {
let tmpHeight = flowDirection === 'south' || flowDirection === 'north' ? moduleHeight : moduleWidth
width =
moduleSetupSurface.from === 'roofCover'
+roofSizeSet === 1
? calculateVisibleModuleHeight(tmpWidth, tmpHeight, getDegreeByChon(moduleSetupSurfaces[i].roofMaterial.pitch), flowDirection).width
: tmpWidth
height =
moduleSetupSurface.from === 'roofCover'
+roofSizeSet === 1
? calculateVisibleModuleHeight(tmpWidth, tmpHeight, getDegreeByChon(moduleSetupSurfaces[i].roofMaterial.pitch), flowDirection).height
: tmpHeight
@ -1386,11 +1387,11 @@ export function useModuleBasicSetting(tabNum) {
//복시도, 실치수에 따른 모듈 높이 조정
width =
trestlePolygon.from === 'roofCover'
+roofSizeSet === 1
? calculateVisibleModuleHeight(tmpWidth, tmpHeight, getDegreeByChon(trestlePolygon.roofMaterial.pitch), flowDirection).width
: tmpWidth
height =
trestlePolygon.from === 'roofCover'
+roofSizeSet === 1
? calculateVisibleModuleHeight(tmpWidth, tmpHeight, getDegreeByChon(trestlePolygon.roofMaterial.pitch), flowDirection).height
: tmpHeight
@ -2974,11 +2975,11 @@ export function useModuleBasicSetting(tabNum) {
const pointY2 = top
//디버깅
const finalLine = new QLine([pointX1, pointY1, pointX2, pointY2], {
stroke: 'red',
strokeWidth: 1,
selectable: true,
})
// const finalLine = new QLine([pointX1, pointY1, pointX2, pointY2], {
// stroke: 'red',
// strokeWidth: 1,
// selectable: true,
// })
// canvas?.add(finalLine)
// canvas?.renderAll()
@ -3106,11 +3107,11 @@ export function useModuleBasicSetting(tabNum) {
const pointY2 = coords[2].y + ((coords[2].x - top) / (coords[2].x - coords[1].x)) * (coords[1].y - coords[2].y)
//디버깅용
const finalLine = new QLine([pointX1, pointY1, pointX2, pointY2], {
stroke: 'red',
strokeWidth: 1,
selectable: true,
})
// const finalLine = new QLine([pointX1, pointY1, pointX2, pointY2], {
// stroke: 'red',
// strokeWidth: 1,
// selectable: true,
// })
// canvas?.add(finalLine)
// canvas?.renderAll()
@ -3308,7 +3309,7 @@ export function useModuleBasicSetting(tabNum) {
let tmpHeight = flowDirection === 'south' || flowDirection === 'north' ? moduleHeight : moduleWidth
let { width, height } =
moduleSetupSurface.from === 'roofCover'
+roofSizeSet === 1
? calculateVisibleModuleHeight(tmpWidth, tmpHeight, getDegreeByChon(moduleSetupSurfaces[i].roofMaterial.pitch), flowDirection)
: { width: tmpWidth, height: tmpHeight }
@ -4027,7 +4028,7 @@ export function useModuleBasicSetting(tabNum) {
10
}
return moduleSetupSurface.from === 'roofCover'
return +roofSizeSet === 1
? calculateVisibleModuleHeight(tmpWidth, tmpHeight, getDegreeByChon(moduleSetupSurface.roofMaterial.pitch), moduleSetupSurface.direction)
: { width: tmpWidth, height: tmpHeight }
}

View File

@ -82,7 +82,6 @@ export const useTrestle = () => {
}
let rackInfos = []
if (rack) {
rackInfos = Object.keys(rack).map((key) => {
return { key, value: rack[key] }
@ -2484,8 +2483,8 @@ export const useTrestle = () => {
// 각도에 따른 길이 반환
function getTrestleLength(length, degree, surface) {
if (surface.from !== 'roofCover') {
// 지붕덮개로부터 온게 아니면 그냥 length 리턴
if (+roofSizeSet !== 1) {
// 복시도 입력이 아닌경우 그냥 길이 return
return length
}
const radians = (degree * Math.PI) / 180

View File

@ -18,7 +18,6 @@ import {
basicSettingState,
correntObjectNoState,
corridorDimensionSelector,
fetchRoofMaterialsState,
roofMaterialsAtom,
selectedRoofMaterialSelector,
settingModalFirstOptionsState,
@ -41,6 +40,8 @@ import { useCanvasPopupStatusController } from '@/hooks/common/useCanvasPopupSta
import { v4 as uuidv4 } from 'uuid'
import { useEvent } from '@/hooks/useEvent'
import { logger } from '@/util/logger'
import { useText } from '@/hooks/useText'
import { usePolygon } from '@/hooks/usePolygon'
const defaultDotLineGridSetting = {
INTERVAL: {
@ -118,7 +119,6 @@ export function useCanvasSetting(executeEffect = true) {
const { getRoofMaterialList, getModuleTypeItemList } = useMasterController()
const [roofMaterials, setRoofMaterials] = useRecoilState(roofMaterialsAtom)
const [addedRoofs, setAddedRoofs] = useRecoilState(addedRoofsState)
const [fetchRoofMaterials, setFetchRoofMaterials] = useRecoilState(fetchRoofMaterialsState)
const setCurrentMenu = useSetRecoilState(currentMenuState)
const resetModuleSelectionData = useResetRecoilState(moduleSelectionDataState) /* 다음으로 넘어가는 최종 데이터 */
@ -133,6 +133,9 @@ export function useCanvasSetting(executeEffect = true) {
const { addPopup } = usePopup()
const [popupId, setPopupId] = useState(uuidv4())
const { changeCorridorDimensionText } = useText()
const { setPolygonLinesActualSize } = usePolygon()
const SelectOptions = [
{ id: 1, name: getMessage('modal.canvas.setting.grid.dot.line.setting.line.origin'), value: 1 },
{ id: 2, name: '1/2', value: 1 / 2 },
@ -197,7 +200,7 @@ export function useCanvasSetting(executeEffect = true) {
}
}, [addedRoofs])
useEffect(() => {
/*useEffect(() => {
if (!executeEffect) {
return
}
@ -212,7 +215,7 @@ export function useCanvasSetting(executeEffect = true) {
setAddedRoofs(newAddedRoofs)
}
setBasicSettings({ ...basicSetting, selectedRoofMaterial: selectedRoofMaterial })
}, [roofMaterials])
}, [roofMaterials])*/
useEffect(() => {
if (!canvas) {
@ -221,39 +224,13 @@ export function useCanvasSetting(executeEffect = true) {
if (!executeEffect) {
return
}
const { column } = corridorDimension
const lengthTexts = canvas.getObjects().filter((obj) => obj.name === 'lengthText')
const group = canvas.getObjects().filter((obj) => obj.type === 'group')
group.forEach((obj) => {
obj._objects
.filter((obj2) => obj2.name === 'lengthText')
.forEach((obj3) => {
lengthTexts.push(obj3)
})
})
switch (column) {
case 'corridorDimension':
lengthTexts.forEach((obj) => {
if (obj.planeSize) {
obj.set({ text: obj.planeSize.toString() })
}
})
break
case 'realDimension':
lengthTexts.forEach((obj) => {
if (obj.actualSize) {
obj.set({ text: obj.actualSize.toString() })
}
})
break
case 'noneDimension':
lengthTexts.forEach((obj) => {
obj.set({ text: '' })
})
break
const roofs = canvasObjects.filter((obj) => obj.name === POLYGON_TYPE.ROOF)
if (roofs.length > 0) {
roofs.forEach((roof) => {
setPolygonLinesActualSize(roof)
})
changeCorridorDimensionText()
}
canvas?.renderAll()
}, [corridorDimension])
useEffect(() => {
@ -448,17 +425,18 @@ export function useCanvasSetting(executeEffect = true) {
}
if (addRoofs.length > 0) {
setAddedRoofs(addRoofs)
setBasicSettings({
...basicSetting,
roofMaterials: addRoofs[0],
planNo: roofsRow[0].planNo,
roofSizeSet: roofsRow[0].roofSizeSet,
roofAngleSet: roofsRow[0].roofAngleSet,
roofsData: roofsArray,
selectedRoofMaterial: addRoofs.find((roof) => roof.selected),
setBasicSettings((prev) => {
return {
...basicSetting,
roofMaterials: addRoofs[0],
planNo: roofsRow[0].planNo,
roofSizeSet: roofsRow[0].roofSizeSet,
roofAngleSet: roofsRow[0].roofAngleSet,
roofsData: roofsArray,
selectedRoofMaterial: addRoofs.find((roof) => roof.selected),
}
})
setAddedRoofs(addRoofs)
setCanvasSetting({
...basicSetting,

View File

@ -28,6 +28,7 @@ import { outerLinePointsState } from '@/store/outerLineAtom'
import { QcastContext } from '@/app/QcastProvider'
import { usePlan } from '@/hooks/usePlan'
import { roofsState } from '@/store/roofAtom'
import { useText } from '@/hooks/useText'
export function useRoofAllocationSetting(id) {
const canvas = useRecoilValue(canvasState)
@ -60,6 +61,7 @@ export function useRoofAllocationSetting(id) {
const [moduleSelectionData, setModuleSelectionData] = useRecoilState(moduleSelectionDataState)
const resetPoints = useResetRecoilState(outerLinePointsState)
const [corridorDimension, setCorridorDimension] = useRecoilState(corridorDimensionSelector)
const { changeCorridorDimensionText } = useText()
useEffect(() => {
/** 배치면 초기설정에서 선택한 지붕재 배열 설정 */
@ -127,9 +129,9 @@ export function useRoofAllocationSetting(id) {
}
})
} else {
if(roofList.length > 0){
if (roofList.length > 0) {
roofsArray = roofList
}else{
} else {
roofsArray = [
{
planNo: planNo,
@ -188,7 +190,6 @@ export function useRoofAllocationSetting(id) {
})
//데이터 동기화
setCurrentRoofList(selectRoofs)
})
} catch (error) {
console.error('Data fetching error:', error)
@ -459,6 +460,10 @@ export function useRoofAllocationSetting(id) {
/** 모듈 선택 데이터 초기화 */
// modifyModuleSelectionData()
setModuleSelectionData({ ...moduleSelectionData, roofConstructions: newRoofList })
setTimeout(() => {
changeCorridorDimensionText('realDimension')
}, 500)
}
/**

View File

@ -1,9 +1,8 @@
'use client'
import { useEffect } from 'react'
import { useRecoilState, useRecoilValue, useResetRecoilState } from 'recoil'
import { useRecoilValue, useResetRecoilState } from 'recoil'
import { canvasSettingState, canvasState, currentCanvasPlanState, globalPitchState } from '@/store/canvasAtom'
import { MENU, POLYGON_TYPE, LINE_TYPE } from '@/common/common'
import { LINE_TYPE, MENU, POLYGON_TYPE } from '@/common/common'
import { getIntersectionPoint, toFixedWithoutRounding } from '@/util/canvas-util'
import { degreesToRadians } from '@turf/turf'
import { QPolygon } from '@/components/fabric/QPolygon'
@ -20,13 +19,12 @@ import { useRoofFn } from '@/hooks/common/useRoofFn'
import { outerLinePointsState } from '@/store/outerLineAtom'
import { placementShapeDrawingPointsState } from '@/store/placementShapeDrawingAtom'
import { getBackGroundImage } from '@/lib/imageActions'
import PlacementSurfaceLineProperty from '@/components/floor-plan/modal/placementShape/PlacementSurfaceLineProperty'
import { v4 as uuidv4 } from 'uuid'
import { useCanvasSetting } from '@/hooks/option/useCanvasSetting'
import { useText } from '@/hooks/useText'
export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
const { getMessage } = useMessage()
const { drawDirectionArrow, addPolygon } = usePolygon()
const { drawDirectionArrow, addPolygon, addLengthText } = usePolygon()
const lengthTextFont = useRecoilValue(fontSelector('lengthText'))
const resetOuterLinePoints = useResetRecoilState(outerLinePointsState)
const resetPlacementShapeDrawingPoints = useResetRecoilState(placementShapeDrawingPointsState)
@ -40,6 +38,7 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
// const { addCanvasMouseEventListener, initEvent } = useContext(EventContext)
const { addPopup, closePopup } = usePopup()
const { setSurfaceShapePattern } = useRoofFn()
const { changeCorridorDimensionText } = useText()
const currentCanvasPlan = useRecoilValue(currentCanvasPlanState)
const { fetchSettings } = useCanvasSetting(false)
@ -848,7 +847,7 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
const selection = new fabric.ActiveSelection(selectionArray, {
canvas: canvas,
draggable: true,
// draggable: true,
lockMovementX: false, // X축 이동 허용
lockMovementY: false, // Y축 이동 허용
originX: 'center',
@ -858,7 +857,7 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
canvas.setActiveObject(selection)
addCanvasMouseEventListener('mouse:up', (e) => {
canvas.selection = true
canvas.selection = false
canvas.discardActiveObject() // 모든 선택 해제
canvas.requestRenderAll() // 화면 업데이트
@ -875,10 +874,13 @@ export function useSurfaceShapeBatch({ isHidden, setIsHidden }) {
}
})
canvas.renderAll()
roof.fire('polygonMoved')
// roof.fire('polygonMoved')
roof.fire('modified')
drawDirectionArrow(roof)
changeCorridorDimensionText()
addLengthText(roof)
initEvent()
canvas.renderAll()
})
}
}

View File

@ -5,6 +5,8 @@ import { canvasSizeState, canvasState, canvasZoomState, currentMenuState, curren
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'
// 캔버스에 필요한 이벤트
export function useCanvasEvent() {
@ -15,6 +17,8 @@ export function useCanvasEvent() {
const [canvasZoom, setCanvasZoom] = useRecoilState(canvasZoomState)
const lengthTextOption = useRecoilValue(fontSelector('lengthText'))
const currentMenu = useRecoilValue(currentMenuState)
const { changeCorridorDimensionText } = useText()
const { setPolygonLinesActualSize } = usePolygon()
useEffect(() => {
canvas?.setZoom(canvasZoom / 100)
@ -63,6 +67,13 @@ export function useCanvasEvent() {
textObjs.forEach((obj) => {
obj.bringToFront()
})
if (target.name === POLYGON_TYPE.ROOF) {
setTimeout(() => {
setPolygonLinesActualSize(target)
changeCorridorDimensionText()
}, 300)
}
}
if (target.name === 'cell') {
@ -116,48 +127,6 @@ export function useCanvasEvent() {
target.setControlVisible(controlKey, false)
})
})
/*target.on('editing:exited', () => {
if (isNaN(target.text.trim())) {
target.set({ text: previousValue })
canvas?.renderAll()
return
}
const updatedValue = parseFloat(target.text.trim())
const targetParent = target.parent
const points = targetParent.getCurrentPoints()
const i = target.idx // Assuming target.index gives the index of the point
const startPoint = points[i]
const endPoint = points[(i + 1) % points.length]
const dx = endPoint.x - startPoint.x
const dy = endPoint.y - startPoint.y
const currentLength = Math.sqrt(dx * dx + dy * dy)
const scaleFactor = updatedValue / currentLength
const newEndPoint = {
x: startPoint.x + dx * scaleFactor,
y: startPoint.y + dy * scaleFactor,
}
const newPoints = [...points]
newPoints[(i + 1) % points.length] = newEndPoint
for (let idx = i + 1; idx < points.length; idx++) {
if (newPoints[idx].x === endPoint.x) {
newPoints[idx].x = newEndPoint.x
} else if (newPoints[idx].y === endPoint.y) {
newPoints[idx].y = newEndPoint.y
}
}
const newPolygon = new QPolygon(newPoints, targetParent.initOptions)
canvas?.add(newPolygon)
canvas?.remove(targetParent)
canvas?.renderAll()
})*/
target.on('moving', (e) => {
target.uuid = uuidv4()

View File

@ -10,7 +10,7 @@ import {
selectedModelsState,
seriesState,
} from '@/store/circuitTrestleAtom'
import { moduleSelectionDataState, selectedModuleState } from '@/store/selectedModuleOptions'
import { selectedModuleState } from '@/store/selectedModuleOptions'
import { useContext, useEffect } from 'react'
import { useRecoilState, useRecoilValue, useResetRecoilState, useSetRecoilState } from 'recoil'
import { useMessage } from './useMessage'
@ -101,7 +101,11 @@ export function useCircuitTrestle(executeEffect = false) {
// result 배열에서 roofSurface 값을 기준으로 순서대로 정렬한다.
return groupSort(result)
if (pcsCheck.division) {
return groupSort(result)
} else {
return result
}
}
const groupSort = (arr) => {

View File

@ -129,7 +129,7 @@ export function useEvent() {
let arrivalPoint = { x: pointer.x, y: pointer.y }
if (adsorptionPointMode) {
const roofsPoints = roofs.map((roof) => roof.points).flat()
const roofsPoints = roofs.map((roof) => roof.getCurrentPoints()).flat()
roofAdsorptionPoints.current = [...roofsPoints]
const auxiliaryLines = canvas.getObjects().filter((obj) => obj.name === 'auxiliaryLine')

View File

@ -1,15 +1,18 @@
import { useRecoilValue } from 'recoil'
import {
ANGLE_TYPE,
canvasState,
currentAngleTypeSelector,
fontFamilyState,
fontSizeState,
globalPitchState,
pitchTextSelector,
showAngleUnitSelector,
} from '@/store/canvasAtom'
import { QLine } from '@/components/fabric/QLine'
import { getChonByDegree, getDegreeByChon } from '@/util/canvas-util'
import { basicSettingState } from '@/store/settingAtom'
import { calcLineActualSize } from '@/util/qpolygon-utils'
import { getDegreeByChon } from '@/util/canvas-util'
import { useText } from '@/hooks/useText'
export const useLine = () => {
const canvas = useRecoilValue(canvasState)
@ -18,6 +21,10 @@ export const useLine = () => {
const currentAngleType = useRecoilValue(currentAngleTypeSelector)
const pitchText = useRecoilValue(pitchTextSelector)
const angleUnit = useRecoilValue(showAngleUnitSelector)
const roofSizeSet = useRecoilValue(basicSettingState).roofSizeSet
const globalPitch = useRecoilValue(globalPitchState)
const { changeCorridorDimensionText } = useText()
const addLine = (points = [], options) => {
const line = new QLine(points, {
@ -151,6 +158,57 @@ export const useLine = () => {
})
}
/**
* 복도치수, 실제치수에 따라 actualSize를 설정한다.
* @param line
* @param direction polygon의 방향
* @param pitch
*/
const setActualSize = (line, direction, pitch = globalPitch) => {
const { x1, y1, x2, y2 } = line
const isHorizontal = y1 === y2
const isVertical = x1 === x2
const isDiagonal = !isHorizontal && !isVertical
const lineLength = line.getLength()
line.attributes = { ...line.attributes, planeSize: line.getLength(), actualSize: line.getLength() }
if (+roofSizeSet === 1) {
if (direction === 'south' || direction === 'north') {
if (isVertical) {
line.attributes = {
...line.attributes,
actualSize: calcLineActualSize(line, getDegreeByChon(pitch)),
}
} else if (isDiagonal) {
const yLength = Math.abs(y2 - y1) * 10
const h = yLength * Math.tan(getDegreeByChon(pitch) * (Math.PI / 180))
const actualSize = Math.sqrt(h ** 2 + lineLength ** 2)
line.attributes = { ...line.attributes, actualSize: actualSize }
}
} else if (direction === 'west' || direction === 'east') {
if (isHorizontal) {
line.attributes = {
...line.attributes,
actualSize: calcLineActualSize(line, getDegreeByChon(pitch)),
}
} else if (isDiagonal) {
const xLength = Math.abs(x2 - x1) * 10
const h = xLength * Math.tan(getDegreeByChon(pitch) * (Math.PI / 180))
const actualSize = Math.sqrt(h ** 2 + lineLength ** 2)
line.attributes = { ...line.attributes, actualSize: actualSize }
}
}
}
line.attributes = { ...line.attributes, actualSize: Number(line.attributes.actualSize.toFixed(0)) }
}
return {
addLine,
removeLine,
@ -160,5 +218,6 @@ export const useLine = () => {
removePitchText,
addPitchTextsByOuterLines,
getLengthByLine,
setActualSize,
}
}

View File

@ -4,7 +4,7 @@ import { fabric } from 'fabric'
import { calculateIntersection, findAndRemoveClosestPoint, getDegreeByChon, getDegreeInOrientation, isPointOnLine } from '@/util/canvas-util'
import { QPolygon } from '@/components/fabric/QPolygon'
import { isSamePoint, removeDuplicatePolygons } from '@/util/qpolygon-utils'
import { flowDisplaySelector } from '@/store/settingAtom'
import { basicSettingState, flowDisplaySelector } from '@/store/settingAtom'
import { fontSelector } from '@/store/fontAtom'
import { QLine } from '@/components/fabric/QLine'
import { LINE_TYPE, POLYGON_TYPE } from '@/common/common'
@ -18,6 +18,9 @@ export const usePolygon = () => {
const currentAngleType = useRecoilValue(currentAngleTypeSelector)
const pitchText = useRecoilValue(pitchTextSelector)
const globalPitch = useRecoilValue(globalPitchState)
const roofSizeSet = useRecoilValue(basicSettingState).roofSizeSet
const { setActualSize } = useLine()
const { getLengthByLine } = useLine()
@ -86,27 +89,30 @@ export const usePolygon = () => {
const maxY = line.top + line.length
const degree = (Math.atan2(y2 - y1, x2 - x1) * 180) / Math.PI
const text = new fabric.Textbox(planeSize ? planeSize.toString() : length.toString(), {
left: left,
top: top,
fontSize: lengthTextFontOptions.fontSize.value,
minX,
maxX,
minY,
maxY,
parentDirection: line.direction,
parentDegree: degree,
parentId: polygon.id,
planeSize: planeSize ?? length,
actualSize: actualSize ?? length,
editable: false,
selectable: true,
lockRotation: true,
lockScalingX: true,
lockScalingY: true,
parent: polygon,
name: 'lengthText',
})
const text = new fabric.Textbox(
+roofSizeSet === 1 ? (actualSize ? actualSize.toString() : length.toString()) : planeSize ? planeSize.toString() : length.toString(),
{
left: left,
top: top,
fontSize: lengthTextFontOptions.fontSize.value,
minX,
maxX,
minY,
maxY,
parentDirection: line.direction,
parentDegree: degree,
parentId: polygon.id,
planeSize: planeSize ?? length,
actualSize: actualSize ?? length,
editable: false,
selectable: true,
lockRotation: true,
lockScalingX: true,
lockScalingY: true,
parent: polygon,
name: 'lengthText',
},
)
polygon.texts.push(text)
canvas.add(text)
})
@ -921,12 +927,69 @@ export const usePolygon = () => {
}
return !shouldRemove
})
// 중복된 라인들을 canvas에서 제거
linesToRemove.forEach((line) => {
canvas.remove(line)
})
// innerLines가 합쳐졌을 때 polygonLine과 같은 경우 그 polygonLine의 need를 false로 변경
const mergeOverlappingInnerLines = (lines) => {
const mergedLines = []
const processed = new Set()
lines.forEach((line, index) => {
if (processed.has(index)) return
let currentLine = { ...line }
processed.add(index)
// 현재 라인과 겹치는 다른 라인들을 찾아서 합치기
for (let i = index + 1; i < lines.length; i++) {
if (processed.has(i)) continue
const otherLine = lines[i]
if (checkLineOverlap(currentLine, otherLine)) {
// 두 라인을 합치기 - 가장 긴 범위로 확장
const isVertical = Math.abs(currentLine.x1 - currentLine.x2) < 1
if (isVertical) {
const allYPoints = [currentLine.y1, currentLine.y2, otherLine.y1, otherLine.y2]
currentLine.y1 = Math.min(...allYPoints)
currentLine.y2 = Math.max(...allYPoints)
currentLine.x1 = currentLine.x2 = (currentLine.x1 + otherLine.x1) / 2
} else {
const allXPoints = [currentLine.x1, currentLine.x2, otherLine.x1, otherLine.x2]
currentLine.x1 = Math.min(...allXPoints)
currentLine.x2 = Math.max(...allXPoints)
currentLine.y1 = currentLine.y2 = (currentLine.y1 + otherLine.y1) / 2
}
processed.add(i)
}
}
mergedLines.push(currentLine)
})
return mergedLines
}
const mergedInnerLines = mergeOverlappingInnerLines(innerLines)
// 합쳐진 innerLine과 동일한 polygonLine의 need를 false로 설정
polygonLines.forEach((polygonLine) => {
mergedInnerLines.forEach((mergedInnerLine) => {
const isSameLine =
(isSamePoint(polygonLine.startPoint, mergedInnerLine.startPoint) && isSamePoint(polygonLine.endPoint, mergedInnerLine.endPoint)) ||
(isSamePoint(polygonLine.startPoint, mergedInnerLine.endPoint) && isSamePoint(polygonLine.endPoint, mergedInnerLine.startPoint))
if (isSameLine) {
polygonLine.need = false
}
})
})
canvas.renderAll()
/*polygonLines.forEach((line) => {
@ -934,6 +997,7 @@ export const usePolygon = () => {
canvas.add(line)
})
canvas.renderAll()*/
polygonLines = polygonLines.filter((line) => line.need)
polygonLines.forEach((line) => {
/*const originStroke = line.stroke
@ -1589,8 +1653,8 @@ export const usePolygon = () => {
const remainingLines = [...allLines] // 사용 가능한 line들의 복사본
// isStart가 true인 line들만 시작점으로 사용
const startLines = remainingLines.filter(line => line.attributes?.isStart === true)
const startLines = remainingLines.filter((line) => line.attributes?.isStart === true)
startLines.forEach((startLine) => {
// 현재 남아있는 line들로 그래프 생성
const graph = {}
@ -1615,13 +1679,13 @@ export const usePolygon = () => {
const startPoint = { ...startLine.startPoint } // 시작점
let arrivalPoint = { ...startLine.endPoint } // 도착점
const roof = getPath(startPoint, arrivalPoint, graph)
if (roof.length > 0) {
roofs.push(roof)
// 사용된 startLine을 remainingLines에서 제거
const startLineIndex = remainingLines.findIndex(line => line === startLine)
const startLineIndex = remainingLines.findIndex((line) => line === startLine)
if (startLineIndex !== -1) {
remainingLines.splice(startLineIndex, 1)
}
@ -1675,6 +1739,22 @@ export const usePolygon = () => {
canvas.renderAll()
}
/**
* 폴리곤의 라인 속성을 복도치수, 실제치수에 따라 actualSize 설정
* @param polygon
*/
const setPolygonLinesActualSize = (polygon) => {
if (!polygon.lines || polygon.lines.length === 0 || !polygon.roofMaterial) {
return
}
polygon.lines.forEach((line) => {
setActualSize(line, polygon.direction, +polygon.roofMaterial?.pitch)
})
addLengthText(polygon)
}
return {
addPolygon,
addPolygonByLines,
@ -1683,5 +1763,6 @@ export const usePolygon = () => {
addLengthText,
splitPolygonWithLines,
splitPolygonWithSeparate,
setPolygonLinesActualSize,
}
}

51
src/hooks/useText.js Normal file
View File

@ -0,0 +1,51 @@
import { useRecoilValue } from 'recoil'
import { corridorDimensionSelector } from '@/store/settingAtom'
import { canvasState } from '@/store/canvasAtom'
export function useText() {
const canvas = useRecoilValue(canvasState)
const corridorDimension = useRecoilValue(corridorDimensionSelector)
const changeCorridorDimensionText = (columnText) => {
let { column } = corridorDimension
if (columnText) {
column = columnText
}
const lengthTexts = canvas.getObjects().filter((obj) => obj.name === 'lengthText')
const group = canvas.getObjects().filter((obj) => obj.type === 'group')
group.forEach((obj) => {
obj._objects
.filter((obj2) => obj2.name === 'lengthText')
.forEach((obj3) => {
lengthTexts.push(obj3)
})
})
switch (column) {
case 'corridorDimension':
lengthTexts.forEach((obj) => {
if (obj.planeSize) {
obj.set({ text: obj.planeSize.toString() })
}
})
break
case 'realDimension':
lengthTexts.forEach((obj) => {
if (obj.actualSize) {
obj.set({ text: obj.actualSize.toString() })
}
})
break
case 'noneDimension':
lengthTexts.forEach((obj) => {
obj.set({ text: '' })
})
break
}
canvas?.renderAll()
}
return {
changeCorridorDimensionText,
}
}

View File

@ -0,0 +1,113 @@
import CircularNode from "./CircularNode";
export interface ICircularList {
readonly Size: number;
AddNext(node: CircularNode, newNode: CircularNode): void;
AddPrevious(node: CircularNode, newNode: CircularNode): void;
AddLast(node: CircularNode): void;
Remove(node: CircularNode): void;
}
export default class CircularList<T extends CircularNode> implements ICircularList {
private _first: T = null;
private _size: number = 0;
public AddNext(node: CircularNode, newNode: CircularNode) {
if (newNode.List !== null)
throw new Error("Node is already assigned to different list!");
newNode.List = this;
newNode.Previous = node;
newNode.Next = node.Next;
node.Next.Previous = newNode;
node.Next = newNode;
this._size++;
}
AddPrevious(node: CircularNode, newNode: CircularNode) {
if (newNode.List !== null)
throw new Error("Node is already assigned to different list!");
newNode.List = this;
newNode.Previous = node.Previous;
newNode.Next = node;
node.Previous.Next = newNode;
node.Previous = newNode;
this._size++;
}
AddLast(node: CircularNode) {
if (node.List !== null)
throw new Error("Node is already assigned to different list!");
if (this._first === null) {
this._first = node as T;
node.List = this;
node.Next = node;
node.Previous = node;
this._size++;
} else
this.AddPrevious(this._first, node);
}
Remove(node: CircularNode) {
if (node.List !== this)
throw new Error("Node is not assigned to this list!");
if (this._size <= 0)
throw new Error("List is empty can't remove!");
node.List = null;
if (this._size === 1)
this._first = null;
else {
if (this._first === node)
this._first = <T>this._first.Next;
node.Previous.Next = node.Next;
node.Next.Previous = node.Previous;
}
node.Previous = null;
node.Next = null;
this._size--;
}
public get Size(): number {
return this._size;
}
public First(): T {
return this._first;
}
public* Iterate(): Generator<T> {
let current = this._first;
let i = 0;
while (current !== null) {
yield current;
if (++i === this.Size) {
return;
}
current = <T>current.Next;
}
}
}

View File

@ -0,0 +1,19 @@
import {ICircularList} from "./CircularList";
export default class CircularNode {
public List: ICircularList = null;
public Next: CircularNode = null;
public Previous: CircularNode = null;
public AddNext(node: CircularNode) {
this.List.AddNext(this, node);
}
public AddPrevious(node: CircularNode) {
this.List.AddPrevious(this, node);
}
public Remove() {
this.List.Remove(this);
}
}

View File

@ -0,0 +1,28 @@
import CircularNode from "./CircularNode";
import Vector2d from "../Primitives/Vector2d";
import LineLinear2d from "../Primitives/LineLinear2d";
import LineParametric2d from "../Primitives/LineParametric2d";
export default class Edge extends CircularNode {
public readonly Begin: Vector2d;
public readonly End: Vector2d;
public readonly Norm: Vector2d;
public readonly LineLinear2d: LineLinear2d;
public BisectorNext: LineParametric2d = null;
public BisectorPrevious: LineParametric2d = null;
constructor(begin: Vector2d, end: Vector2d) {
super();
this.Begin = begin;
this.End = end;
this.LineLinear2d = new LineLinear2d(begin, end);
this.Norm = end.Sub(begin).Normalized();
}
public ToString(): string {
return `Edge [p1=${this.Begin}, p2=${this.End}]`;
}
}

View File

@ -0,0 +1,39 @@
import CircularNode from "./CircularNode";
import Vector2d from "../Primitives/Vector2d";
import LineParametric2d from "../Primitives/LineParametric2d";
import Edge from "./Edge";
import {FaceNode} from "../Path/FaceNode";
export default class Vertex extends CircularNode {
readonly RoundDigitCount = 5;
public Point: Vector2d = null;
public readonly Distance: number;
public readonly Bisector: LineParametric2d = null;
public readonly NextEdge: Edge = null;
public readonly PreviousEdge: Edge = null;
public LeftFace: FaceNode = null;
public RightFace: FaceNode = null;
public IsProcessed: boolean;
constructor(point: Vector2d, distance: number, bisector: LineParametric2d, previousEdge: Edge, nextEdge: Edge) {
super();
this.Point = point;
this.Distance = +distance.toFixed(this.RoundDigitCount);
this.Bisector = bisector;
this.PreviousEdge = previousEdge;
this.NextEdge = nextEdge;
this.IsProcessed = false;
}
public ToString(): string {
return "Vertex [v=" + this.Point + ", IsProcessed=" + this.IsProcessed +
", Bisector=" + this.Bisector + ", PreviousEdge=" + this.PreviousEdge +
", NextEdge=" + this.NextEdge;
}
}

View File

@ -0,0 +1,13 @@
import Edge from "./Circular/Edge";
import Vector2d from "./Primitives/Vector2d";
import {List} from "./Utils";
export default class EdgeResult {
public readonly Edge: Edge;
public readonly Polygon: List<Vector2d>;
constructor(edge: Edge, polygon: List<Vector2d>) {
this.Edge = edge;
this.Polygon = polygon;
}
}

View File

@ -0,0 +1,7 @@
enum ChainType {
Edge,
ClosedEdge,
Split
}
export default ChainType;

View File

@ -0,0 +1,40 @@
import IChain from "./IChain";
import EdgeEvent from "../EdgeEvent";
import {List} from "../../Utils";
import Edge from "../../Circular/Edge";
import Vertex from "../../Circular/Vertex";
import ChainType from "./ChainType";
export default class EdgeChain implements IChain {
private readonly _closed: boolean;
public EdgeList: List<EdgeEvent>;
constructor(edgeList: List<EdgeEvent>) {
this.EdgeList = edgeList;
this._closed = this.PreviousVertex === this.NextVertex;
}
public get PreviousEdge(): Edge {
return this.EdgeList[0].PreviousVertex.PreviousEdge;
}
public get NextEdge(): Edge {
return this.EdgeList[this.EdgeList.Count - 1].NextVertex.NextEdge;
}
public get PreviousVertex(): Vertex {
return this.EdgeList[0].PreviousVertex;
}
public get NextVertex(): Vertex {
return this.EdgeList[this.EdgeList.Count - 1].NextVertex;
}
public get CurrentVertex(): Vertex {
return null;
}
public get ChainType(): ChainType {
return this._closed ? ChainType.ClosedEdge : ChainType.Edge;
}
}

View File

@ -0,0 +1,17 @@
import Edge from "../../Circular/Edge";
import Vertex from "../../Circular/Vertex";
import ChainType from "./ChainType";
export default interface IChain {
get PreviousEdge(): Edge;
get NextEdge(): Edge;
get PreviousVertex(): Vertex;
get NextVertex(): Vertex;
get CurrentVertex(): Vertex;
get ChainType(): ChainType;
}

View File

@ -0,0 +1,40 @@
import IChain from "./IChain";
import Edge from "../../Circular/Edge";
import Vertex from "../../Circular/Vertex";
import ChainType from "./ChainType";
export default class SingleEdgeChain implements IChain {
private readonly _nextVertex: Vertex;
private readonly _oppositeEdge: Edge;
private readonly _previousVertex: Vertex;
constructor(oppositeEdge: Edge, nextVertex: Vertex) {
this._oppositeEdge = oppositeEdge;
this._nextVertex = nextVertex;
this._previousVertex = nextVertex.Previous as Vertex;
}
public get PreviousEdge(): Edge {
return this._oppositeEdge;
}
public get NextEdge(): Edge {
return this._oppositeEdge;
}
public get PreviousVertex(): Vertex {
return this._previousVertex;
}
public get NextVertex(): Vertex {
return this._nextVertex;
}
public get CurrentVertex(): Vertex {
return null;
}
public get ChainType(): ChainType {
return ChainType.Split;
}
}

View File

@ -0,0 +1,45 @@
import IChain from "./IChain";
import Edge from "../../Circular/Edge";
import Vertex from "../../Circular/Vertex";
import ChainType from "./ChainType";
import VertexSplitEvent from "../VertexSplitEvent";
import SplitEvent from "../SplitEvent";
export default class SplitChain implements IChain {
private readonly _splitEvent: SplitEvent;
constructor(event: SplitEvent) {
this._splitEvent = event;
}
public get OppositeEdge(): Edge {
if (!(this._splitEvent instanceof VertexSplitEvent))
return this._splitEvent.OppositeEdge;
return null;
}
public get PreviousEdge(): Edge {
return this._splitEvent.Parent.PreviousEdge;
}
public get NextEdge(): Edge {
return this._splitEvent.Parent.NextEdge;
}
public get PreviousVertex(): Vertex {
return this._splitEvent.Parent.Previous as Vertex;
}
public get NextVertex(): Vertex {
return this._splitEvent.Parent.Next as Vertex;
}
public get CurrentVertex(): Vertex {
return this._splitEvent.Parent;
}
public get ChainType(): ChainType {
return ChainType.Split;
}
}

View File

@ -0,0 +1,27 @@
import SkeletonEvent from "./SkeletonEvent";
import Vertex from "../Circular/Vertex";
import Vector2d from "../Primitives/Vector2d";
export default class EdgeEvent extends SkeletonEvent {
public readonly NextVertex: Vertex;
public readonly PreviousVertex: Vertex;
public override get IsObsolete(): boolean {
return this.PreviousVertex.IsProcessed || this.NextVertex.IsProcessed;
}
constructor(point: Vector2d, distance: number, previousVertex: Vertex, nextVertex: Vertex) {
super(point, distance);
this.PreviousVertex = previousVertex;
this.NextVertex = nextVertex;
}
public override ToString(): string {
return "EdgeEvent [V=" + this.V + ", PreviousVertex="
+ (this.PreviousVertex !== null ? this.PreviousVertex.Point.ToString() : "null") +
", NextVertex="
+ (this.NextVertex !== null ? this.NextVertex.Point.ToString() : "null") + ", Distance=" +
this.Distance + "]";
}
}

View File

@ -0,0 +1,17 @@
import SkeletonEvent from "./SkeletonEvent";
import Vector2d from "../Primitives/Vector2d";
import EdgeChain from "./Chains/EdgeChain";
export default class MultiEdgeEvent extends SkeletonEvent {
public readonly Chain: EdgeChain;
public override get IsObsolete(): boolean {
return false;
}
constructor(point: Vector2d, distance: number, chain: EdgeChain) {
super(point, distance);
this.Chain = chain;
}
}

View File

@ -0,0 +1,18 @@
import SkeletonEvent from "./SkeletonEvent";
import {List} from "../Utils";
import IChain from "./Chains/IChain";
import Vector2d from "../Primitives/Vector2d";
export default class MultiSplitEvent extends SkeletonEvent {
public readonly Chains: List<IChain>;
public override get IsObsolete(): boolean {
return false;
}
constructor(point: Vector2d, distance: number, chains: List<IChain>) {
super(point, distance);
this.Chains = chains;
}
}

View File

@ -0,0 +1,17 @@
import SkeletonEvent from "./SkeletonEvent";
import Vector2d from "../Primitives/Vector2d";
import EdgeChain from "./Chains/EdgeChain";
export default class PickEvent extends SkeletonEvent {
public readonly Chain: EdgeChain;
public override get IsObsolete(): boolean {
return false;
}
constructor(point: Vector2d, distance: number, chain: EdgeChain) {
super(point, distance);
this.Chain = chain;
}
}

View File

@ -0,0 +1,22 @@
import Vector2d from "../Primitives/Vector2d";
export default abstract class SkeletonEvent {
public V: Vector2d = null;
public Distance: number;
public abstract get IsObsolete(): boolean;
protected constructor(point: Vector2d, distance: number) {
this.V = point;
this.Distance = distance;
}
public ToString(): string {
return "IntersectEntry [V=" + this.V + ", Distance=" + this.Distance + "]";
}
public GetType(): string {
return this.constructor.name;
}
}

View File

@ -0,0 +1,26 @@
import SkeletonEvent from "./SkeletonEvent";
import Edge from "../Circular/Edge";
import Vertex from "../Circular/Vertex";
import Vector2d from "../Primitives/Vector2d";
export default class SplitEvent extends SkeletonEvent {
public readonly OppositeEdge: Edge = null;
public readonly Parent: Vertex = null;
constructor(point: Vector2d, distance: number, parent: Vertex, oppositeEdge: Edge) {
super(point, distance);
this.Parent = parent;
this.OppositeEdge = oppositeEdge;
}
public override get IsObsolete(): boolean {
return this.Parent.IsProcessed;
}
public override ToString(): string {
return "SplitEvent [V=" + this.V + ", Parent=" + (this.Parent !== null ? this.Parent.Point.ToString() : "null") +
", Distance=" + this.Distance + "]";
}
}

View File

@ -0,0 +1,15 @@
import SplitEvent from "./SplitEvent";
import Vector2d from "../Primitives/Vector2d";
import Vertex from "../Circular/Vertex";
export default class VertexSplitEvent extends SplitEvent {
constructor(point: Vector2d, distance: number, parent: Vertex) {
super(point, distance, parent, null);
}
public override ToString(): string {
return "VertexSplitEvent [V=" + this.V + ", Parent=" +
(this.Parent !== null ? this.Parent.Point.ToString() : "null")
+ ", Distance=" + this.Distance + "]";
}
}

View File

@ -0,0 +1,56 @@
import Vertex from "./Circular/Vertex";
import {List} from "./Utils";
import CircularList from "./Circular/CircularList";
export default class LavUtil {
public static IsSameLav(v1: Vertex, v2: Vertex): boolean {
if (v1.List === null || v2.List === null)
return false;
return v1.List === v2.List;
}
public static RemoveFromLav(vertex: Vertex) {
if (vertex === null || vertex.List === null)
return;
vertex.Remove();
}
public static CutLavPart(startVertex: Vertex, endVertex: Vertex): List<Vertex> {
const ret = new List<Vertex>();
const size = startVertex.List.Size;
let next = startVertex;
for (let i = 0; i < size; i++) {
const current = next;
next = current.Next as Vertex;
current.Remove();
ret.Add(current);
if (current === endVertex)
return ret;
}
throw new Error("End vertex can't be found in start vertex lav");
}
public static MergeBeforeBaseVertex(base: Vertex, merged: Vertex) {
const size = merged.List.Size;
for (let i = 0; i < size; i++) {
const nextMerged = merged.Next as Vertex;
nextMerged.Remove();
base.AddPrevious(nextMerged);
}
}
public static MoveAllVertexToLavEnd(vertex: Vertex, newLaw: CircularList<Vertex>) {
const size = vertex.List.Size;
for (let i = 0; i < size; i++) {
const ver = vertex;
vertex = vertex.Next as Vertex;
ver.Remove();
newLaw.AddLast(ver);
}
}
}

View File

@ -0,0 +1,24 @@
import PathQueueNode from "./PathQueueNode";
import Vertex from "../Circular/Vertex";
import FaceQueue from "./FaceQueue";
export class FaceNode extends PathQueueNode<FaceNode> {
public readonly Vertex: Vertex = null;
constructor(vertex: Vertex) {
super();
this.Vertex = vertex;
}
public get FaceQueue(): FaceQueue {
return <FaceQueue>this.List;
}
public get IsQueueUnconnected(): boolean {
return this.FaceQueue.IsUnconnected;
}
public QueueClose() {
this.FaceQueue.Close();
}
}

View File

@ -0,0 +1,24 @@
import PathQueue from "./PathQueue";
import {FaceNode} from "./FaceNode";
import PathQueueNode from "./PathQueueNode";
import Edge from "../Circular/Edge";
export default class FaceQueue extends PathQueue<FaceNode> {
public Edge: Edge = null;
public Closed: boolean = false;
public get IsUnconnected(): boolean {
return this.Edge === null;
}
public override AddPush(node: PathQueueNode<FaceNode>, newNode: PathQueueNode<FaceNode>) {
if (this.Closed)
throw new Error("Can't add node to closed FaceQueue");
super.AddPush(node, newNode);
}
public Close() {
this.Closed = true;
}
}

View File

@ -0,0 +1,39 @@
import {FaceNode} from "./FaceNode";
export default class FaceQueueUtil {
public static ConnectQueues(firstFace: FaceNode, secondFace: FaceNode) {
if (firstFace.List === null)
throw new Error("firstFace.list cannot be null.");
if (secondFace.List === null)
throw new Error("secondFace.list cannot be null.");
if (firstFace.List === secondFace.List) {
if (!firstFace.IsEnd || !secondFace.IsEnd)
throw new Error("try to connect the same list not on end nodes");
if (firstFace.IsQueueUnconnected || secondFace.IsQueueUnconnected)
throw new Error("can't close node queue not conected with edges");
firstFace.QueueClose();
return;
}
if (!firstFace.IsQueueUnconnected && !secondFace.IsQueueUnconnected)
throw new Error(
"can't connect two diffrent queues if each of them is connected to edge");
if (!firstFace.IsQueueUnconnected) {
const qLeft = secondFace.FaceQueue;
this.MoveNodes(firstFace, secondFace);
qLeft.Close();
} else {
const qRight = firstFace.FaceQueue;
this.MoveNodes(secondFace, firstFace);
qRight.Close();
}
}
private static MoveNodes(firstFace: FaceNode, secondFace: FaceNode) {
firstFace.AddQueue(secondFace);
}
}

View File

@ -0,0 +1,103 @@
import PathQueueNode from "./PathQueueNode";
export default class PathQueue<T extends PathQueueNode<T>> {
public Size: number = 0;
public First: PathQueueNode<T> = null;
public AddPush(node: PathQueueNode<T>, newNode: PathQueueNode<T>) {
if (newNode.List !== null)
throw new Error("Node is already assigned to different list!");
if (node.Next !== null && node.Previous !== null)
throw new Error("Can't push new node. Node is inside a Quere. " +
"New node can by added only at the end of queue.");
newNode.List = this;
this.Size++;
if (node.Next === null) {
newNode.Previous = node;
newNode.Next = null;
node.Next = newNode;
} else {
newNode.Previous = null;
newNode.Next = node;
node.Previous = newNode;
}
}
public AddFirst(node: T) {
if (node.List !== null)
throw new Error("Node is already assigned to different list!");
if (this.First === null) {
this.First = node;
node.List = this;
node.Next = null;
node.Previous = null;
this.Size++;
} else
throw new Error("First element already exist!");
}
public Pop(node: PathQueueNode<T>): PathQueueNode<T> {
if (node.List !== this)
throw new Error("Node is not assigned to this list!");
if (this.Size <= 0)
throw new Error("List is empty can't remove!");
if (!node.IsEnd)
throw new Error("Can pop only from end of queue!");
node.List = null;
let previous: PathQueueNode<T> = null;
if (this.Size === 1)
this.First = null;
else {
if (this.First === node) {
if (node.Next !== null)
this.First = node.Next;
else if (node.Previous !== null)
this.First = node.Previous;
else
throw new Error("Ups ?");
}
if (node.Next !== null) {
node.Next.Previous = null;
previous = node.Next;
} else if (node.Previous !== null) {
node.Previous.Next = null;
previous = node.Previous;
}
}
node.Previous = null;
node.Next = null;
this.Size--;
return previous;
}
public* Iterate(): Generator<T> {
let current: T = <T>(this.First !== null ? this.First.FindEnd() : null);
let i = 0;
while (current !== null)
{
yield current;
if (++i === this.Size)
return;
current = <T>current.Next;
}
}
}

View File

@ -0,0 +1,51 @@
import PathQueue from "./PathQueue";
export default class PathQueueNode<T extends PathQueueNode<T>> {
public List: PathQueue<T> = null;
public Next: PathQueueNode<T> = null;
public Previous: PathQueueNode<T> = null;
public get IsEnd(): boolean {
return this.Next === null || this.Previous === null;
}
public AddPush(node: PathQueueNode<T>) {
this.List.AddPush(this, node);
}
public AddQueue(queue: PathQueueNode<T>): PathQueueNode<T> {
if (this.List === queue.List)
return null;
let currentQueue: PathQueueNode<T> = this;
let current = queue;
while (current !== null) {
const next = current.Pop();
currentQueue.AddPush(current);
currentQueue = current;
current = next;
}
return currentQueue;
}
public FindEnd(): PathQueueNode<T> {
if (this.IsEnd)
return this;
let current: PathQueueNode<T> = this;
while (current.Previous !== null)
current = current.Previous;
return current;
}
public Pop(): PathQueueNode<T> {
return this.List.Pop(this);
}
}

View File

@ -0,0 +1,41 @@
import Vector2d from "./Vector2d";
export default class LineLinear2d {
public A: number;
public B: number;
public C: number;
constructor(pP1: Vector2d = Vector2d.Empty, pP2: Vector2d = Vector2d.Empty) {
this.A = pP1.Y - pP2.Y;
this.B = pP2.X - pP1.X;
this.C = pP1.X * pP2.Y - pP2.X * pP1.Y;
}
public SetFromCoefficients(a: number, b: number, c: number): LineLinear2d {
this.A = a;
this.B = b;
this.C = c;
return this;
}
public Collide(pLine: LineLinear2d): Vector2d {
return LineLinear2d.Collide(this, pLine);
}
public static Collide(pLine1: LineLinear2d, pLine2: LineLinear2d): Vector2d {
return LineLinear2d.CollideCoeff(pLine1.A, pLine1.B, pLine1.C, pLine2.A, pLine2.B, pLine2.C);
}
public static CollideCoeff(A1: number, B1: number, C1: number, A2: number, B2: number, C2: number): Vector2d {
const WAB = A1 * B2 - A2 * B1;
const WBC = B1 * C2 - B2 * C1;
const WCA = C1 * A2 - C2 * A1;
return WAB === 0 ? Vector2d.Empty : new Vector2d(WBC / WAB, WCA / WAB);
}
public Contains(point: Vector2d): boolean {
return Math.abs((point.X * this.A + point.Y * this.B + this.C)) < Number.EPSILON;
}
}

View File

@ -0,0 +1,47 @@
import Vector2d from "./Vector2d";
import LineLinear2d from "./LineLinear2d";
import PrimitiveUtils from "./PrimitiveUtils";
export default class LineParametric2d {
public static readonly Empty: LineParametric2d = new LineParametric2d(Vector2d.Empty, Vector2d.Empty);
public A: Vector2d = null;
public U: Vector2d = null;
constructor(pA: Vector2d, pU: Vector2d) {
this.A = pA;
this.U = pU;
}
public CreateLinearForm(): LineLinear2d {
const x = this.A.X;
const y = this.A.Y;
const B = -this.U.X;
const A = this.U.Y;
const C = -(A * x + B * y);
return new LineLinear2d().SetFromCoefficients(A, B, C);
}
public static Collide(ray: LineParametric2d, line: LineLinear2d, epsilon: number): Vector2d {
const collide = LineLinear2d.Collide(ray.CreateLinearForm(), line);
if (collide.Equals(Vector2d.Empty)) {
return Vector2d.Empty;
}
const collideVector = collide.Sub(ray.A);
return ray.U.Dot(collideVector) < epsilon ? Vector2d.Empty : collide;
}
public IsOnLeftSite(point: Vector2d, epsilon: number): boolean {
const direction = point.Sub(this.A);
return PrimitiveUtils.OrthogonalRight(this.U).Dot(direction) < epsilon;
}
public IsOnRightSite(point: Vector2d, epsilon: number): boolean {
const direction = point.Sub(this.A);
return PrimitiveUtils.OrthogonalRight(this.U).Dot(direction) > -epsilon;
}
}

View File

@ -0,0 +1,241 @@
import Vector2d from "./Vector2d";
import LineParametric2d from "./LineParametric2d";
import {List} from "../Utils";
class IntersectPoints {
public readonly Intersect: Vector2d = null;
public readonly IntersectEnd: Vector2d = null;
constructor(intersect?: Vector2d, intersectEnd?: Vector2d) {
if (!intersect) {
intersect = Vector2d.Empty;
}
if (!intersectEnd) {
intersectEnd = Vector2d.Empty;
}
this.Intersect = intersect;
this.IntersectEnd = intersectEnd;
}
}
export default class PrimitiveUtils {
public static FromTo(begin: Vector2d, end: Vector2d): Vector2d {
return new Vector2d(end.X - begin.X, end.Y - begin.Y);
}
public static OrthogonalLeft(v: Vector2d): Vector2d {
return new Vector2d(-v.Y, v.X);
}
public static OrthogonalRight(v: Vector2d): Vector2d {
return new Vector2d(v.Y, -v.X);
}
public static OrthogonalProjection(unitVector: Vector2d, vectorToProject: Vector2d): Vector2d {
const n = new Vector2d(unitVector.X, unitVector.Y).Normalized();
const px = vectorToProject.X;
const py = vectorToProject.Y;
const ax = n.X;
const ay = n.Y;
return new Vector2d(px * ax * ax + py * ax * ay, px * ax * ay + py * ay * ay);
}
public static BisectorNormalized(norm1: Vector2d, norm2: Vector2d): Vector2d {
const e1v = PrimitiveUtils.OrthogonalLeft(norm1);
const e2v = PrimitiveUtils.OrthogonalLeft(norm2);
if (norm1.Dot(norm2) > 0)
return e1v.Add(e2v);
let ret = new Vector2d(norm1.X, norm1.Y);
ret.Negate();
ret = ret.Add(norm2);
if (e1v.Dot(norm2) < 0)
ret.Negate();
return ret;
}
private static readonly SmallNum = 0.00000001;
private static readonly Empty: IntersectPoints = new IntersectPoints();
public static IsPointOnRay(point: Vector2d, ray: LineParametric2d, epsilon: number): boolean {
const rayDirection = new Vector2d(ray.U.X, ray.U.Y).Normalized();
const pointVector = point.Sub(ray.A);
let dot = rayDirection.Dot(pointVector);
if (dot < epsilon)
return false;
const x = rayDirection.X;
rayDirection.X = rayDirection.Y;
rayDirection.Y = -x;
dot = rayDirection.Dot(pointVector);
return -epsilon < dot && dot < epsilon;
}
public static IntersectRays2D(r1: LineParametric2d, r2: LineParametric2d): IntersectPoints {
const s1p0 = r1.A;
const s1p1 = r1.A.Add(r1.U);
const s2p0 = r2.A;
const u = r1.U;
const v = r2.U;
const w = s1p0.Sub(s2p0);
const d = PrimitiveUtils.Perp(u, v);
if (Math.abs(d) < PrimitiveUtils.SmallNum) {
if (PrimitiveUtils.Perp(u, w) !== 0 || PrimitiveUtils.Perp(v, w) !== 0)
return PrimitiveUtils.Empty;
const du = PrimitiveUtils.Dot(u, u);
const dv = PrimitiveUtils.Dot(v, v);
if (du === 0 && dv === 0) {
if (s1p0.NotEquals(s2p0))
return PrimitiveUtils.Empty;
return new IntersectPoints(s1p0);
}
if (du === 0) {
if (!PrimitiveUtils.InCollinearRay(s1p0, s2p0, v))
return PrimitiveUtils.Empty;
return new IntersectPoints(s1p0);
}
if (dv === 0) {
if (!PrimitiveUtils.InCollinearRay(s2p0, s1p0, u))
return PrimitiveUtils.Empty;
return new IntersectPoints(s2p0);
}
let t0, t1;
var w2 = s1p1.Sub(s2p0);
if (v.X !== 0) {
t0 = w.X / v.X;
t1 = w2.X / v.X;
} else {
t0 = w.Y / v.Y;
t1 = w2.Y / v.Y;
}
if (t0 > t1) {
const t = t0;
t0 = t1;
t1 = t;
}
if (t1 < 0)
return PrimitiveUtils.Empty;
t0 = t0 < 0 ? 0 : t0;
if (t0 === t1) {
let I0 = new Vector2d(v.X, v.Y);
I0 = I0.MultiplyScalar(t0);
I0 = I0.Add(s2p0);
return new IntersectPoints(I0);
}
let I_0 = new Vector2d(v.X, v.Y);
I_0 = I_0.MultiplyScalar(t0);
I_0 = I_0.Add(s2p0);
let I1 = new Vector2d(v.X, v.Y);
I1 = I1.MultiplyScalar(t1);
I1 = I1.Add(s2p0);
return new IntersectPoints(I_0, I1);
}
const sI = PrimitiveUtils.Perp(v, w) / d;
if (sI < 0 /* || sI > 1 */)
return PrimitiveUtils.Empty;
const tI = PrimitiveUtils.Perp(u, w) / d;
if (tI < 0 /* || tI > 1 */)
return PrimitiveUtils.Empty;
let IO = new Vector2d(u.X, u.Y);
IO = IO.MultiplyScalar(sI);
IO = IO.Add(s1p0);
return new IntersectPoints(IO);
}
private static InCollinearRay(p: Vector2d, rayStart: Vector2d, rayDirection: Vector2d): boolean {
const collideVector = p.Sub(rayStart);
const dot = rayDirection.Dot(collideVector);
return !(dot < 0);
}
private static Dot(u: Vector2d, v: Vector2d): number {
return u.Dot(v);
}
private static Perp(u: Vector2d, v: Vector2d): number {
return u.X * v.Y - u.Y * v.X;
}
public static IsClockwisePolygon(polygon: List<Vector2d>): boolean {
return PrimitiveUtils.Area(polygon) < 0;
}
private static Area(polygon: List<Vector2d>): number {
const n = polygon.Count;
let A = 0;
for (let p = n - 1, q = 0; q < n; p = q++)
A += polygon[p].X * polygon[q].Y - polygon[q].X * polygon[p].Y;
return A * 0.5;
}
public static MakeCounterClockwise(polygon: List<Vector2d>): List<Vector2d> {
if (PrimitiveUtils.IsClockwisePolygon(polygon))
polygon.Reverse();
return polygon;
}
public static IsPointInsidePolygon(point: Vector2d, points: List<Vector2d>): boolean {
const numpoints = points.Count;
if (numpoints < 3)
return false;
let it = 0;
const first = points[it];
let oddNodes = false;
for (let i = 0; i < numpoints; i++) {
const node1 = points[it];
it++;
const node2 = i === numpoints - 1 ? first : points[it];
const x = point.X;
const y = point.Y;
if (node1.Y < y && node2.Y >= y || node2.Y < y && node1.Y >= y) {
if (node1.X + (y - node1.Y) / (node2.Y - node1.Y) * (node2.X - node1.X) < x)
oddNodes = !oddNodes;
}
}
return oddNodes;
}
}

View File

@ -0,0 +1,63 @@
import {IComparer, List} from "../Utils";
export default class PriorityQueue<T> {
private readonly _comparer: IComparer<T> = null;
private readonly _heap: List<T> = null;
constructor(capacity: number, comparer: IComparer<T>) {
this._heap = new List<T>(capacity);
this._comparer = comparer;
}
public Clear() {
this._heap.Clear();
}
public Add(item: T) {
let n = this._heap.Count;
this._heap.Add(item);
while (n !== 0) {
const p = Math.floor(n / 2);
if (this._comparer.Compare(this._heap[n], (this._heap[p])) >= 0) break;
const tmp: T = this._heap[n];
this._heap[n] = this._heap[p];
this._heap[p] = tmp;
n = p;
}
}
get Count(): number {
return this._heap.Count;
}
get Empty(): boolean {
return this._heap.Count === 0;
}
public Peek(): T {
return !this._heap.Any() ? null : this._heap[0];
}
public Next(): T {
const val: T = this._heap[0];
const nMax = this._heap.Count - 1;
this._heap[0] = this._heap[nMax];
this._heap.RemoveAt(nMax);
let p = 0;
while (true) {
let c = p * 2;
if (c >= nMax) break;
if (c + 1 < nMax && this._comparer.Compare(this._heap[c + 1], this._heap[c]) < 0) c++;
if (this._comparer.Compare(this._heap[p], (this._heap[c])) <= 0) break;
const tmp: T = this._heap[p];
this._heap[p] = this._heap[c];
this._heap[c] = tmp;
p = c;
}
return val;
}
}

View File

@ -0,0 +1,61 @@
export default class Vector2d {
public static Empty: Vector2d = new Vector2d(Number.MIN_VALUE, Number.MIN_VALUE);
public X: number = 0;
public Y: number = 0;
constructor(x: number, y: number) {
this.X = x;
this.Y = y;
}
public Negate() {
this.X = -this.X;
this.Y = -this.Y;
}
public DistanceTo(var1: Vector2d): number {
const var2 = this.X - var1.X;
const var4 = this.Y - var1.Y;
return Math.sqrt(var2 * var2 + var4 * var4);
}
public Normalized(): Vector2d {
const var1 = 1 / Math.sqrt(this.X * this.X + this.Y * this.Y);
return new Vector2d(this.X * var1, this.Y * var1);
}
public Dot(var1: Vector2d): number {
return this.X * var1.X + this.Y * var1.Y;
}
public DistanceSquared(var1: Vector2d): number {
const var2 = this.X - var1.X;
const var4 = this.Y - var1.Y;
return var2 * var2 + var4 * var4;
}
public Add(v: Vector2d): Vector2d {
return new Vector2d(this.X + v.X, this.Y + v.Y);
}
public Sub(v: Vector2d): Vector2d {
return new Vector2d(this.X - v.X, this.Y - v.Y);
}
public MultiplyScalar(scale: number): Vector2d {
return new Vector2d(this.X * scale, this.Y * scale);
}
public Equals(v: Vector2d): boolean {
return this.X === v.X && this.Y === v.Y;
}
public NotEquals(v: Vector2d): boolean {
return !this.Equals(v);
}
public ToString(): string {
return `${this.X}, ${this.Y}`;
}
}

View File

@ -0,0 +1,13 @@
import Vector2d from "./Primitives/Vector2d";
import EdgeResult from "./EdgeResult";
import {Dictionary, List} from "./Utils";
export class Skeleton {
public readonly Edges: List<EdgeResult> = null;
public readonly Distances: Dictionary<Vector2d, number> = null;
constructor(edges: List<EdgeResult>, distances: Dictionary<Vector2d, number>) {
this.Edges = edges;
this.Distances = distances;
}
}

View File

@ -0,0 +1,994 @@
import {Skeleton} from "./Skeleton";
import {HashSet, List, IComparer, Dictionary, GeoJSONMultipolygon} from "./Utils";
import Vector2d from "./Primitives/Vector2d";
import PriorityQueue from "./Primitives/PriorityQueue";
import Edge from "./Circular/Edge";
import Vertex from "./Circular/Vertex";
import CircularList from "./Circular/CircularList";
import FaceQueue from "./Path/FaceQueue";
import SkeletonEvent from "./Events/SkeletonEvent";
import FaceQueueUtil from "./Path/FaceQueueUtil";
import LavUtil from "./LavUtil";
import IChain from "./Events/Chains/IChain";
import PrimitiveUtils from "./Primitives/PrimitiveUtils";
import LineParametric2d from "./Primitives/LineParametric2d";
import {FaceNode} from "./Path/FaceNode";
import MultiEdgeEvent from "./Events/MultiEdgeEvent";
import EdgeEvent from "./Events/EdgeEvent";
import PickEvent from "./Events/PickEvent";
import MultiSplitEvent from "./Events/MultiSplitEvent";
import SingleEdgeChain from "./Events/Chains/SingleEdgeChain";
import SplitChain from "./Events/Chains/SplitChain";
import SplitEvent from "./Events/SplitEvent";
import VertexSplitEvent from "./Events/VertexSplitEvent";
import EdgeChain from "./Events/Chains/EdgeChain";
import LineLinear2d from "./Primitives/LineLinear2d";
import EdgeResult from "./EdgeResult";
import ChainType from "./Events/Chains/ChainType";
export default class SkeletonBuilder {
private static readonly SplitEpsilon = 1e-10;
public static BuildFromGeoJSON(multipolygon: GeoJSONMultipolygon): Skeleton {
const allEdges: List<EdgeResult> = new List();
const allDistances: Dictionary<Vector2d, number> = new Dictionary();
for (const polygon of multipolygon) {
if (polygon.length > 0) {
const outer = this.ListFromCoordinatesArray(polygon[0]);
const holes: List<List<Vector2d>> = new List();
for (let i = 1; i < polygon.length; i++) {
holes.Add(this.ListFromCoordinatesArray(polygon[i]));
}
const skeleton = this.Build(outer, holes);
for (const edge of skeleton.Edges) {
allEdges.Add(edge);
}
for (const [key, distance] of skeleton.Distances.entries()) {
allDistances.Add(key, distance);
}
}
}
return new Skeleton(allEdges, allDistances);
}
private static ListFromCoordinatesArray(arr: [number, number][]): List<Vector2d> {
const list: List<Vector2d> = new List();
for (const [x, y] of arr) {
list.Add(new Vector2d(x, y));
}
return list;
}
public static Build(polygon: List<Vector2d>, holes: List<List<Vector2d>> = null): Skeleton {
polygon = this.InitPolygon(polygon);
holes = this.MakeClockwise(holes);
const queue = new PriorityQueue<SkeletonEvent>(3, new SkeletonEventDistanseComparer());
const sLav = new HashSet<CircularList<Vertex>>();
const faces = new List<FaceQueue>();
const edges = new List<Edge>();
this.InitSlav(polygon, sLav, edges, faces);
if (holes !== null) {
for (const inner of holes) {
this.InitSlav(inner, sLav, edges, faces);
}
}
this.InitEvents(sLav, queue, edges);
let count = 0;
while (!queue.Empty) {
count = this.AssertMaxNumberOfInteraction(count);
const levelHeight = queue.Peek().Distance;
for (const event of this.LoadAndGroupLevelEvents(queue)) {
if (event.IsObsolete)
continue;
if (event instanceof EdgeEvent)
throw new Error("All edge@events should be converted to MultiEdgeEvents for given level");
if (event instanceof SplitEvent)
throw new Error("All split events should be converted to MultiSplitEvents for given level");
if (event instanceof MultiSplitEvent)
this.MultiSplitEvent(<MultiSplitEvent>event, sLav, queue, edges);
else if (event instanceof PickEvent)
this.PickEvent(<PickEvent>event);
else if (event instanceof MultiEdgeEvent)
this.MultiEdgeEvent(<MultiEdgeEvent>event, queue, edges);
else
throw new Error("Unknown event type: " + event.GetType());
}
this.ProcessTwoNodeLavs(sLav);
this.RemoveEventsUnderHeight(queue, levelHeight);
this.RemoveEmptyLav(sLav);
}
return this.AddFacesToOutput(faces);
}
private static InitPolygon(polygon: List<Vector2d>): List<Vector2d> {
if (polygon === null)
throw new Error("polygon can't be null");
if (polygon[0].Equals(polygon[polygon.Count - 1]))
throw new Error("polygon can't start and end with the same point");
return this.MakeCounterClockwise(polygon);
}
private static ProcessTwoNodeLavs(sLav: HashSet<CircularList<Vertex>>) {
for (const lav of sLav) {
if (lav.Size === 2) {
const first = lav.First();
const last = first.Next as Vertex;
FaceQueueUtil.ConnectQueues(first.LeftFace, last.RightFace);
FaceQueueUtil.ConnectQueues(first.RightFace, last.LeftFace);
first.IsProcessed = true;
last.IsProcessed = true;
LavUtil.RemoveFromLav(first);
LavUtil.RemoveFromLav(last);
}
}
}
private static RemoveEmptyLav(sLav: HashSet<CircularList<Vertex>>) {
sLav.RemoveWhere(circularList => circularList.Size === 0);
}
private static MultiEdgeEvent(event: MultiEdgeEvent, queue: PriorityQueue<SkeletonEvent>, edges: List<Edge>) {
const center = event.V;
const edgeList = event.Chain.EdgeList;
const previousVertex = event.Chain.PreviousVertex;
previousVertex.IsProcessed = true;
const nextVertex = event.Chain.NextVertex;
nextVertex.IsProcessed = true;
const bisector = this.CalcBisector(center, previousVertex.PreviousEdge, nextVertex.NextEdge);
const edgeVertex = new Vertex(center, event.Distance, bisector, previousVertex.PreviousEdge,
nextVertex.NextEdge);
this.AddFaceLeft(edgeVertex, previousVertex);
this.AddFaceRight(edgeVertex, nextVertex);
previousVertex.AddPrevious(edgeVertex);
this.AddMultiBackFaces(edgeList, edgeVertex);
this.ComputeEvents(edgeVertex, queue, edges);
}
private static AddMultiBackFaces(edgeList: List<EdgeEvent>, edgeVertex: Vertex) {
for (const edgeEvent of edgeList) {
const leftVertex = edgeEvent.PreviousVertex;
leftVertex.IsProcessed = true;
LavUtil.RemoveFromLav(leftVertex);
const rightVertex = edgeEvent.NextVertex;
rightVertex.IsProcessed = true;
LavUtil.RemoveFromLav(rightVertex);
this.AddFaceBack(edgeVertex, leftVertex, rightVertex);
}
}
private static PickEvent(event: PickEvent) {
const center = event.V;
const edgeList = event.Chain.EdgeList;
const vertex = new Vertex(center, event.Distance, LineParametric2d.Empty, null, null);
vertex.IsProcessed = true;
this.AddMultiBackFaces(edgeList, vertex);
}
private static MultiSplitEvent(event: MultiSplitEvent, sLav: HashSet<CircularList<Vertex>>, queue: PriorityQueue<SkeletonEvent>, edges: List<Edge>) {
const chains = event.Chains;
const center = event.V;
this.CreateOppositeEdgeChains(sLav, chains, center);
chains.Sort(new ChainComparer(center));
let lastFaceNode: FaceNode = null;
let edgeListSize = chains.Count;
for (let i = 0; i < edgeListSize; i++) {
const chainBegin = chains[i];
const chainEnd = chains[(i + 1) % edgeListSize];
const newVertex = this.CreateMultiSplitVertex(chainBegin.NextEdge, chainEnd.PreviousEdge, center, event.Distance);
const beginNextVertex = chainBegin.NextVertex;
const endPreviousVertex = chainEnd.PreviousVertex;
this.CorrectBisectorDirection(newVertex.Bisector, beginNextVertex, endPreviousVertex, chainBegin.NextEdge, chainEnd.PreviousEdge);
if (LavUtil.IsSameLav(beginNextVertex, endPreviousVertex)) {
const lavPart = LavUtil.CutLavPart(beginNextVertex, endPreviousVertex);
const lav = new CircularList<Vertex>();
sLav.Add(lav);
lav.AddLast(newVertex);
for (const vertex of lavPart)
lav.AddLast(vertex);
} else {
LavUtil.MergeBeforeBaseVertex(beginNextVertex, endPreviousVertex);
endPreviousVertex.AddNext(newVertex);
}
this.ComputeEvents(newVertex, queue, edges);
lastFaceNode = this.AddSplitFaces(lastFaceNode, chainBegin, chainEnd, newVertex);
}
edgeListSize = chains.Count;
for (let i = 0; i < edgeListSize; i++) {
const chainBegin = chains[i];
const chainEnd = chains[(i + 1) % edgeListSize];
LavUtil.RemoveFromLav(chainBegin.CurrentVertex);
LavUtil.RemoveFromLav(chainEnd.CurrentVertex);
if (chainBegin.CurrentVertex !== null)
chainBegin.CurrentVertex.IsProcessed = true;
if (chainEnd.CurrentVertex !== null)
chainEnd.CurrentVertex.IsProcessed = true;
}
}
private static CorrectBisectorDirection(bisector: LineParametric2d, beginNextVertex: Vertex, endPreviousVertex: Vertex, beginEdge: Edge, endEdge: Edge) {
const beginEdge2 = beginNextVertex.PreviousEdge;
const endEdge2 = endPreviousVertex.NextEdge;
if (beginEdge !== beginEdge2 || endEdge !== endEdge2)
throw new Error();
if (beginEdge.Norm.Dot(endEdge.Norm) < -0.97) {
const n1 = PrimitiveUtils.FromTo(endPreviousVertex.Point, bisector.A).Normalized();
const n2 = PrimitiveUtils.FromTo(bisector.A, beginNextVertex.Point).Normalized();
const bisectorPrediction = this.CalcVectorBisector(n1, n2);
if (bisector.U.Dot(bisectorPrediction) < 0)
bisector.U.Negate();
}
}
private static AddSplitFaces(lastFaceNode: FaceNode, chainBegin: IChain, chainEnd: IChain, newVertex: Vertex): FaceNode {
if (chainBegin instanceof SingleEdgeChain) {
if (lastFaceNode === null) {
const beginVertex = this.CreateOppositeEdgeVertex(newVertex);
newVertex.RightFace = beginVertex.RightFace;
lastFaceNode = beginVertex.LeftFace;
} else {
if (newVertex.RightFace !== null)
throw new Error("newVertex.RightFace should be null");
newVertex.RightFace = lastFaceNode;
lastFaceNode = null;
}
} else {
const beginVertex = chainBegin.CurrentVertex;
this.AddFaceRight(newVertex, beginVertex);
}
if (chainEnd instanceof SingleEdgeChain) {
if (lastFaceNode === null) {
const endVertex = this.CreateOppositeEdgeVertex(newVertex);
newVertex.LeftFace = endVertex.LeftFace;
lastFaceNode = endVertex.LeftFace;
} else {
if (newVertex.LeftFace !== null)
throw new Error("newVertex.LeftFace should be null.");
newVertex.LeftFace = lastFaceNode;
lastFaceNode = null;
}
} else {
const endVertex = chainEnd.CurrentVertex;
this.AddFaceLeft(newVertex, endVertex);
}
return lastFaceNode;
}
private static CreateOppositeEdgeVertex(newVertex: Vertex): Vertex {
const vertex = new Vertex(newVertex.Point, newVertex.Distance, newVertex.Bisector, newVertex.PreviousEdge, newVertex.NextEdge);
const fn = new FaceNode(vertex);
vertex.LeftFace = fn;
vertex.RightFace = fn;
const rightFace = new FaceQueue();
rightFace.AddFirst(fn);
return vertex;
}
private static CreateOppositeEdgeChains(sLav: HashSet<CircularList<Vertex>>, chains: List<IChain>, center: Vector2d) {
const oppositeEdges = new HashSet<Edge>();
const oppositeEdgeChains = new List<IChain>();
const chainsForRemoval = new List<IChain>();
for (const chain of chains) {
if (chain instanceof SplitChain) {
const splitChain = <SplitChain>chain;
const oppositeEdge = splitChain.OppositeEdge;
if (oppositeEdge !== null && !oppositeEdges.Contains(oppositeEdge)) {
const nextVertex = this.FindOppositeEdgeLav(sLav, oppositeEdge, center);
if (nextVertex !== null)
oppositeEdgeChains.Add(new SingleEdgeChain(oppositeEdge, nextVertex));
else {
this.FindOppositeEdgeLav(sLav, oppositeEdge, center);
chainsForRemoval.Add(chain);
}
oppositeEdges.Add(oppositeEdge);
}
}
}
for (let chain of chainsForRemoval)
chains.Remove(chain);
chains.AddRange(oppositeEdgeChains);
}
private static CreateMultiSplitVertex(nextEdge: Edge, previousEdge: Edge, center: Vector2d, distance: number): Vertex {
const bisector = this.CalcBisector(center, previousEdge, nextEdge);
return new Vertex(center, distance, bisector, previousEdge, nextEdge);
}
private static CreateChains(cluster: List<SkeletonEvent>): List<IChain> {
const edgeCluster = new List<EdgeEvent>();
const splitCluster = new List<SplitEvent>();
const vertexEventsParents = new HashSet<Vertex>();
for (const skeletonEvent of cluster) {
if (skeletonEvent instanceof EdgeEvent)
edgeCluster.Add(<EdgeEvent>skeletonEvent);
else {
if (skeletonEvent instanceof VertexSplitEvent) {
} else if (skeletonEvent instanceof SplitEvent) {
const splitEvent = <SplitEvent>skeletonEvent;
vertexEventsParents.Add(splitEvent.Parent);
splitCluster.Add(splitEvent);
}
}
}
for (let skeletonEvent of cluster) {
if (skeletonEvent instanceof VertexSplitEvent) {
const vertexEvent = <VertexSplitEvent>skeletonEvent;
if (!vertexEventsParents.Contains(vertexEvent.Parent)) {
vertexEventsParents.Add(vertexEvent.Parent);
splitCluster.Add(vertexEvent);
}
}
}
const edgeChains = new List<EdgeChain>();
while (edgeCluster.Count > 0)
edgeChains.Add(new EdgeChain(this.CreateEdgeChain(edgeCluster)));
const chains = new List<IChain>(edgeChains.Count);
for (const edgeChain of edgeChains)
chains.Add(edgeChain);
splitEventLoop:
while (splitCluster.Any()) {
const split = splitCluster[0];
splitCluster.RemoveAt(0);
for (const chain of edgeChains) {
if (this.IsInEdgeChain(split, chain))
continue splitEventLoop; //goto splitEventLoop;
}
chains.Add(new SplitChain(split));
}
return chains;
}
private static IsInEdgeChain(split: SplitEvent, chain: EdgeChain): boolean {
const splitParent = split.Parent;
const edgeList = chain.EdgeList;
return edgeList.Any(edgeEvent => edgeEvent.PreviousVertex === splitParent || edgeEvent.NextVertex === splitParent);
}
private static CreateEdgeChain(edgeCluster: List<EdgeEvent>): List<EdgeEvent> {
const edgeList = new List<EdgeEvent>();
edgeList.Add(edgeCluster[0]);
edgeCluster.RemoveAt(0);
loop:
for (; ;) {
const beginVertex = edgeList[0].PreviousVertex;
const endVertex = edgeList[edgeList.Count - 1].NextVertex;
for (let i = 0; i < edgeCluster.Count; i++) {
const edge = edgeCluster[i];
if (edge.PreviousVertex === endVertex) {
edgeCluster.RemoveAt(i);
edgeList.Add(edge);
//goto loop;
continue loop;
}
if (edge.NextVertex === beginVertex) {
edgeCluster.RemoveAt(i);
edgeList.Insert(0, edge);
//goto loop;
continue loop;
}
}
break;
}
return edgeList;
}
private static RemoveEventsUnderHeight(queue: PriorityQueue<SkeletonEvent>, levelHeight: number) {
while (!queue.Empty) {
if (queue.Peek().Distance > levelHeight + this.SplitEpsilon)
break;
queue.Next();
}
}
private static LoadAndGroupLevelEvents(queue: PriorityQueue<SkeletonEvent>): List<SkeletonEvent> {
const levelEvents = this.LoadLevelEvents(queue);
return this.GroupLevelEvents(levelEvents);
}
private static GroupLevelEvents(levelEvents: List<SkeletonEvent>): List<SkeletonEvent> {
const ret = new List<SkeletonEvent>();
const parentGroup = new HashSet<Vertex>();
while (levelEvents.Count > 0) {
parentGroup.Clear();
const event = levelEvents[0];
levelEvents.RemoveAt(0);
const eventCenter = event.V;
const distance = event.Distance;
this.AddEventToGroup(parentGroup, event);
const cluster = new List<SkeletonEvent>();
cluster.Add(event);
for (let j = 0; j < levelEvents.Count; j++) {
const test = levelEvents[j];
if (this.IsEventInGroup(parentGroup, test)) {
const item = levelEvents[j];
levelEvents.RemoveAt(j);
cluster.Add(item);
this.AddEventToGroup(parentGroup, test);
j--;
} else if (eventCenter.DistanceTo(test.V) < this.SplitEpsilon) {
const item = levelEvents[j];
levelEvents.RemoveAt(j);
cluster.Add(item);
this.AddEventToGroup(parentGroup, test);
j--;
}
}
ret.Add(this.CreateLevelEvent(eventCenter, distance, cluster));
}
return ret;
}
private static IsEventInGroup(parentGroup: HashSet<Vertex>, event: SkeletonEvent): boolean {
if (event instanceof SplitEvent)
return parentGroup.Contains((<SplitEvent>event).Parent);
if (event instanceof EdgeEvent)
return parentGroup.Contains((<EdgeEvent>event).PreviousVertex)
|| parentGroup.Contains((<EdgeEvent>event).NextVertex);
return false;
}
private static AddEventToGroup(parentGroup: HashSet<Vertex>, event: SkeletonEvent) {
if (event instanceof SplitEvent)
parentGroup.Add((<SplitEvent>event).Parent);
else if (event instanceof EdgeEvent) {
parentGroup.Add((<EdgeEvent>event).PreviousVertex);
parentGroup.Add((<EdgeEvent>event).NextVertex);
}
}
private static CreateLevelEvent(eventCenter: Vector2d, distance: number, eventCluster: List<SkeletonEvent>): SkeletonEvent {
const chains = this.CreateChains(eventCluster);
if (chains.Count === 1) {
const chain = chains[0];
if (chain.ChainType === ChainType.ClosedEdge)
return new PickEvent(eventCenter, distance, <EdgeChain>chain);
if (chain.ChainType === ChainType.Edge)
return new MultiEdgeEvent(eventCenter, distance, <EdgeChain>chain);
if (chain.ChainType === ChainType.Split)
return new MultiSplitEvent(eventCenter, distance, chains);
}
if (chains.Any(chain => chain.ChainType === ChainType.ClosedEdge))
throw new Error("Found closed chain of events for single point, but found more then one chain");
return new MultiSplitEvent(eventCenter, distance, chains);
}
private static LoadLevelEvents(queue: PriorityQueue<SkeletonEvent>): List<SkeletonEvent> {
const level = new List<SkeletonEvent>();
let levelStart: SkeletonEvent;
do {
levelStart = queue.Empty ? null : queue.Next();
}
while (levelStart !== null && levelStart.IsObsolete);
if (levelStart === null || levelStart.IsObsolete)
return level;
const levelStartHeight = levelStart.Distance;
level.Add(levelStart);
let event: SkeletonEvent;
while ((event = queue.Peek()) !== null &&
Math.abs(event.Distance - levelStartHeight) < this.SplitEpsilon) {
const nextLevelEvent = queue.Next();
if (!nextLevelEvent.IsObsolete)
level.Add(nextLevelEvent);
}
return level;
}
private static AssertMaxNumberOfInteraction(count: number): number {
count++;
if (count > 10000)
throw new Error("Too many interaction: bug?");
return count;
}
private static MakeClockwise(holes: List<List<Vector2d>>): List<List<Vector2d>> {
if (holes === null)
return null;
const ret = new List<List<Vector2d>>(holes.Count);
for (const hole of holes) {
if (PrimitiveUtils.IsClockwisePolygon(hole))
ret.Add(hole);
else {
hole.Reverse();
ret.Add(hole);
}
}
return ret;
}
private static MakeCounterClockwise(polygon: List<Vector2d>): List<Vector2d> {
return PrimitiveUtils.MakeCounterClockwise(polygon);
}
private static InitSlav(polygon: List<Vector2d>, sLav: HashSet<CircularList<Vertex>>, edges: List<Edge>, faces: List<FaceQueue>) {
const edgesList = new CircularList<Edge>();
const size = polygon.Count;
for (let i = 0; i < size; i++) {
const j = (i + 1) % size;
edgesList.AddLast(new Edge(polygon[i], polygon[j]));
}
for (const edge of edgesList.Iterate()) {
const nextEdge = edge.Next as Edge;
const bisector = this.CalcBisector(edge.End, edge, nextEdge);
edge.BisectorNext = bisector;
nextEdge.BisectorPrevious = bisector;
edges.Add(edge);
}
const lav = new CircularList<Vertex>();
sLav.Add(lav);
for (const edge of edgesList.Iterate()) {
const nextEdge = edge.Next as Edge;
const vertex = new Vertex(edge.End, 0, edge.BisectorNext, edge, nextEdge);
lav.AddLast(vertex);
}
for (const vertex of lav.Iterate()) {
const next = vertex.Next as Vertex;
const rightFace = new FaceNode(vertex);
const faceQueue = new FaceQueue();
faceQueue.Edge = (vertex.NextEdge);
faceQueue.AddFirst(rightFace);
faces.Add(faceQueue);
vertex.RightFace = rightFace;
const leftFace = new FaceNode(next);
rightFace.AddPush(leftFace);
next.LeftFace = leftFace;
}
}
private static AddFacesToOutput(faces: List<FaceQueue>): Skeleton {
const edgeOutputs = new List<EdgeResult>();
const distances = new Dictionary<Vector2d, number>();
for (const face of faces) {
if (face.Size > 0) {
const faceList = new List<Vector2d>();
for (const fn of face.Iterate()) {
const point = fn.Vertex.Point;
faceList.Add(point);
if (!distances.ContainsKey(point))
distances.Add(point, fn.Vertex.Distance);
}
edgeOutputs.Add(new EdgeResult(face.Edge, faceList));
}
}
return new Skeleton(edgeOutputs, distances);
}
private static InitEvents(sLav: HashSet<CircularList<Vertex>>, queue: PriorityQueue<SkeletonEvent>, edges: List<Edge>) {
for (const lav of sLav) {
for (const vertex of lav.Iterate())
this.ComputeSplitEvents(vertex, edges, queue, -1);
}
for (const lav of sLav) {
for (const vertex of lav.Iterate()) {
const nextVertex = vertex.Next as Vertex;
this.ComputeEdgeEvents(vertex, nextVertex, queue);
}
}
}
private static ComputeSplitEvents(vertex: Vertex, edges: List<Edge>, queue: PriorityQueue<SkeletonEvent>, distanceSquared: number) {
const source = vertex.Point;
const oppositeEdges = this.CalcOppositeEdges(vertex, edges);
for (const oppositeEdge of oppositeEdges) {
const point = oppositeEdge.Point;
if (Math.abs(distanceSquared - (-1)) > this.SplitEpsilon) {
if (source.DistanceSquared(point) > distanceSquared + this.SplitEpsilon) {
continue;
}
}
if (oppositeEdge.OppositePoint.NotEquals(Vector2d.Empty)) {
queue.Add(new VertexSplitEvent(point, oppositeEdge.Distance, vertex));
continue;
}
queue.Add(new SplitEvent(point, oppositeEdge.Distance, vertex, oppositeEdge.OppositeEdge));
}
}
private static ComputeEvents(vertex: Vertex, queue: PriorityQueue<SkeletonEvent>, edges: List<Edge>) {
const distanceSquared = this.ComputeCloserEdgeEvent(vertex, queue);
this.ComputeSplitEvents(vertex, edges, queue, distanceSquared);
}
private static ComputeCloserEdgeEvent(vertex: Vertex, queue: PriorityQueue<SkeletonEvent>): number {
const nextVertex = vertex.Next as Vertex;
const previousVertex = vertex.Previous as Vertex;
const point = vertex.Point;
const point1 = this.ComputeIntersectionBisectors(vertex, nextVertex);
const point2 = this.ComputeIntersectionBisectors(previousVertex, vertex);
if (point1.Equals(Vector2d.Empty) && point2.Equals(Vector2d.Empty))
return -1;
let distance1 = Number.MAX_VALUE;
let distance2 = Number.MAX_VALUE;
if (point1.NotEquals(Vector2d.Empty))
distance1 = point.DistanceSquared(point1);
if (point2.NotEquals(Vector2d.Empty))
distance2 = point.DistanceSquared(point2);
if (Math.abs(distance1 - this.SplitEpsilon) < distance2)
queue.Add(this.CreateEdgeEvent(point1, vertex, nextVertex));
if (Math.abs(distance2 - this.SplitEpsilon) < distance1)
queue.Add(this.CreateEdgeEvent(point2, previousVertex, vertex));
return distance1 < distance2 ? distance1 : distance2;
}
private static CreateEdgeEvent(point: Vector2d, previousVertex: Vertex, nextVertex: Vertex): SkeletonEvent {
return new EdgeEvent(point, this.CalcDistance(point, previousVertex.NextEdge), previousVertex, nextVertex);
}
private static ComputeEdgeEvents(previousVertex: Vertex, nextVertex: Vertex, queue: PriorityQueue<SkeletonEvent>) {
const point = this.ComputeIntersectionBisectors(previousVertex, nextVertex);
if (point.NotEquals(Vector2d.Empty))
queue.Add(this.CreateEdgeEvent(point, previousVertex, nextVertex));
}
private static CalcOppositeEdges(vertex: Vertex, edges: List<Edge>): List<SplitCandidate> {
const ret = new List<SplitCandidate>();
for (const edgeEntry of edges) {
const edge = edgeEntry.LineLinear2d;
if (this.EdgeBehindBisector(vertex.Bisector, edge))
continue;
const candidatePoint = this.CalcCandidatePointForSplit(vertex, edgeEntry);
if (candidatePoint !== null)
ret.Add(candidatePoint);
}
ret.Sort(new SplitCandidateComparer());
return ret;
}
private static EdgeBehindBisector(bisector: LineParametric2d, edge: LineLinear2d): boolean {
return LineParametric2d.Collide(bisector, edge, this.SplitEpsilon).Equals(Vector2d.Empty);
}
private static CalcCandidatePointForSplit(vertex: Vertex, edge: Edge): SplitCandidate {
const vertexEdge = this.ChoseLessParallelVertexEdge(vertex, edge);
if (vertexEdge === null)
return null;
const vertexEdteNormNegate = vertexEdge.Norm;
const edgesBisector = this.CalcVectorBisector(vertexEdteNormNegate, edge.Norm);
const edgesCollide = vertexEdge.LineLinear2d.Collide(edge.LineLinear2d);
if (edgesCollide.Equals(Vector2d.Empty))
throw new Error("Ups this should not happen");
const edgesBisectorLine = new LineParametric2d(edgesCollide, edgesBisector).CreateLinearForm();
const candidatePoint = LineParametric2d.Collide(vertex.Bisector, edgesBisectorLine, this.SplitEpsilon);
if (candidatePoint.Equals(Vector2d.Empty))
return null;
if (edge.BisectorPrevious.IsOnRightSite(candidatePoint, this.SplitEpsilon)
&& edge.BisectorNext.IsOnLeftSite(candidatePoint, this.SplitEpsilon)) {
const distance = this.CalcDistance(candidatePoint, edge);
if (edge.BisectorPrevious.IsOnLeftSite(candidatePoint, this.SplitEpsilon))
return new SplitCandidate(candidatePoint, distance, null, edge.Begin);
if (edge.BisectorNext.IsOnRightSite(candidatePoint, this.SplitEpsilon))
return new SplitCandidate(candidatePoint, distance, null, edge.Begin);
return new SplitCandidate(candidatePoint, distance, edge, Vector2d.Empty);
}
return null;
}
private static ChoseLessParallelVertexEdge(vertex: Vertex, edge: Edge): Edge {
const edgeA = vertex.PreviousEdge;
const edgeB = vertex.NextEdge;
let vertexEdge = edgeA;
const edgeADot = Math.abs(edge.Norm.Dot(edgeA.Norm));
const edgeBDot = Math.abs(edge.Norm.Dot(edgeB.Norm));
if (edgeADot + edgeBDot >= 2 - this.SplitEpsilon)
return null;
if (edgeADot > edgeBDot)
vertexEdge = edgeB;
return vertexEdge;
}
private static ComputeIntersectionBisectors(vertexPrevious: Vertex, vertexNext: Vertex): Vector2d {
const bisectorPrevious = vertexPrevious.Bisector;
const bisectorNext = vertexNext.Bisector;
const intersectRays2d = PrimitiveUtils.IntersectRays2D(bisectorPrevious, bisectorNext);
const intersect = intersectRays2d.Intersect;
if (vertexPrevious.Point.Equals(intersect) || vertexNext.Point.Equals(intersect))
return Vector2d.Empty;
return intersect;
}
private static FindOppositeEdgeLav(sLav: HashSet<CircularList<Vertex>>, oppositeEdge: Edge, center: Vector2d): Vertex {
const edgeLavs = this.FindEdgeLavs(sLav, oppositeEdge, null);
return this.ChooseOppositeEdgeLav(edgeLavs, oppositeEdge, center);
}
private static ChooseOppositeEdgeLav(edgeLavs: List<Vertex>, oppositeEdge: Edge, center: Vector2d): Vertex {
if (!edgeLavs.Any())
return null;
if (edgeLavs.Count === 1)
return edgeLavs[0];
const edgeStart = oppositeEdge.Begin;
const edgeNorm = oppositeEdge.Norm;
const centerVector = center.Sub(edgeStart);
const centerDot = edgeNorm.Dot(centerVector);
for (const end of edgeLavs) {
const begin = end.Previous as Vertex;
const beginVector = begin.Point.Sub(edgeStart);
const endVector = end.Point.Sub(edgeStart);
const beginDot = edgeNorm.Dot(beginVector);
const endDot = edgeNorm.Dot(endVector);
if (beginDot < centerDot && centerDot < endDot ||
beginDot > centerDot && centerDot > endDot)
return end;
}
for (const end of edgeLavs) {
const size = end.List.Size;
const points = new List<Vector2d>(size);
let next = end;
for (let i = 0; i < size; i++) {
points.Add(next.Point);
next = next.Next as Vertex;
}
if (PrimitiveUtils.IsPointInsidePolygon(center, points))
return end;
}
throw new Error("Could not find lav for opposite edge, it could be correct but need some test data to check.");
}
private static FindEdgeLavs(sLav: HashSet<CircularList<Vertex>>, oppositeEdge: Edge, skippedLav: CircularList<Vertex>): List<Vertex> {
const edgeLavs = new List<Vertex>();
for (const lav of sLav) {
if (lav === skippedLav)
continue;
const vertexInLav = this.GetEdgeInLav(lav, oppositeEdge);
if (vertexInLav !== null)
edgeLavs.Add(vertexInLav);
}
return edgeLavs;
}
private static GetEdgeInLav(lav: CircularList<Vertex>, oppositeEdge: Edge): Vertex {
for (const node of lav.Iterate())
if (oppositeEdge === node.PreviousEdge ||
oppositeEdge === node.Previous.Next)
return node;
return null;
}
private static AddFaceBack(newVertex: Vertex, va: Vertex, vb: Vertex) {
const fn = new FaceNode(newVertex);
va.RightFace.AddPush(fn);
FaceQueueUtil.ConnectQueues(fn, vb.LeftFace);
}
private static AddFaceRight(newVertex: Vertex, vb: Vertex) {
const fn = new FaceNode(newVertex);
vb.RightFace.AddPush(fn);
newVertex.RightFace = fn;
}
private static AddFaceLeft(newVertex: Vertex, va: Vertex) {
const fn = new FaceNode(newVertex);
va.LeftFace.AddPush(fn);
newVertex.LeftFace = fn;
}
private static CalcDistance(intersect: Vector2d, currentEdge: Edge): number {
const edge = currentEdge.End.Sub(currentEdge.Begin);
const vector = intersect.Sub(currentEdge.Begin);
const pointOnVector = PrimitiveUtils.OrthogonalProjection(edge, vector);
return vector.DistanceTo(pointOnVector);
}
private static CalcBisector(p: Vector2d, e1: Edge, e2: Edge): LineParametric2d {
const norm1 = e1.Norm;
const norm2 = e2.Norm;
const bisector = this.CalcVectorBisector(norm1, norm2);
return new LineParametric2d(p, bisector);
}
private static CalcVectorBisector(norm1: Vector2d, norm2: Vector2d): Vector2d {
return PrimitiveUtils.BisectorNormalized(norm1, norm2);
}
}
class SkeletonEventDistanseComparer implements IComparer<SkeletonEvent> {
public Compare(left: SkeletonEvent, right: SkeletonEvent): number {
if (left.Distance > right.Distance)
return 1;
if (left.Distance < right.Distance)
return -1;
return 0;
}
}
class ChainComparer implements IComparer<IChain> {
private readonly _center: Vector2d;
constructor(center: Vector2d) {
this._center = center;
}
public Compare(x: IChain, y: IChain): number {
if (x === y)
return 0;
const angle1 = ChainComparer.Angle(this._center, x.PreviousEdge.Begin);
const angle2 = ChainComparer.Angle(this._center, y.PreviousEdge.Begin);
return angle1 > angle2 ? 1 : -1;
}
private static Angle(p0: Vector2d, p1: Vector2d): number {
const dx = p1.X - p0.X;
const dy = p1.Y - p0.Y;
return Math.atan2(dy, dx);
}
}
class SplitCandidateComparer implements IComparer<SplitCandidate> {
public Compare(left: SplitCandidate, right: SplitCandidate): number {
if (left.Distance > right.Distance)
return 1;
if (left.Distance < right.Distance)
return -1;
return 0;
}
}
class SplitCandidate {
public readonly Distance: number;
public readonly OppositeEdge: Edge = null;
public readonly OppositePoint: Vector2d = null;
public readonly Point: Vector2d = null;
constructor(point: Vector2d, distance: number, oppositeEdge: Edge, oppositePoint: Vector2d) {
this.Point = point;
this.Distance = distance;
this.OppositeEdge = oppositeEdge;
this.OppositePoint = oppositePoint;
}
}

133
src/lib/skeletons/Utils.ts Normal file
View File

@ -0,0 +1,133 @@
function insertInArray<T>(array: Array<T>, index: number, item: T): Array<T> {
const items = Array.prototype.slice.call(arguments, 2);
return [].concat(array.slice(0, index), items, array.slice(index));
}
export interface IComparable<T> {
CompareTo(other: T): number;
}
export interface IComparer<T> {
Compare(a: T, b: T): number;
}
export type GeoJSONMultipolygon = [number, number][][][];
export class List<T> extends Array<T> {
constructor(capacity = 0) {
super();
}
public Add(item: T) {
this.push(item);
}
public Insert(index: number, item: T) {
const newArr = insertInArray(this, index, item);
this.length = newArr.length;
for(let i = 0; i < newArr.length; i++) {
this[i] = newArr[i];
}
}
public Reverse() {
this.reverse();
}
public Clear() {
this.length = 0;
}
get Count(): number {
return this.length;
}
public Any(filter?: (item: T) => boolean): boolean {
if (!filter) {
filter = T => true;
}
for (const item of this) {
if (filter(item)) {
return true;
}
}
return false;
}
public RemoveAt(index: number) {
this.splice(index, 1);
}
public Remove(itemToRemove: T) {
const newArr = this.filter(item => item !== itemToRemove);
this.length = newArr.length;
for(let i = 0; i < newArr.length; i++) {
this[i] = newArr[i];
}
}
public AddRange(list: List<T>) {
for (const item of list) {
this.Add(item);
}
}
public Sort(comparer: IComparer<T>) {
this.sort(comparer.Compare.bind(comparer));
}
}
export class HashSet<T> implements Iterable<T> {
private Set: Set<T>;
constructor() {
this.Set = new Set();
}
public Add(item: T) {
this.Set.add(item);
}
public Remove(item: T) {
this.Set.delete(item);
}
public RemoveWhere(filter: (item: T) => boolean) {
for (const item of this.Set.values()) {
if (filter(item)) {
this.Set.delete(item);
}
}
}
public Contains(item: T): boolean {
return this.Set.has(item);
}
public Clear() {
this.Set.clear();
}
public* [Symbol.iterator](): Generator<T> {
for (const item of this.Set.values()) {
yield item;
}
}
}
export class Dictionary<T1, T2> extends Map<T1, T2> {
public ContainsKey(key: T1): boolean {
return this.has(key);
}
public Add(key: T1, value: T2) {
return this.set(key, value);
}
}

View File

@ -0,0 +1,12 @@
// Types
export type { GeoJSONMultipolygon, List } from "./Utils";
export type { Skeleton } from "./Skeleton";
// Values
export { default as Vector2d } from "./Primitives/Vector2d";
export { default as SkeletonBuilder } from "./SkeletonBuilder";
export { default as EdgeResult } from "./EdgeResult";
export { default as Edge } from "./Circular/Edge";
export { default as Vertex } from "./Circular/Vertex";

View File

@ -57,6 +57,7 @@
"modal.movement.flow.line.top.right": "高さ変更:上、右",
"plan.menu.roof.cover.outline.edit.offset": "外壁の編集とオフセット",
"plan.menu.roof.cover.roof.surface.alloc": "屋根面の割り当て",
"plan.menu.roof.cover.roof.surface.all.remove": "伏せ図全削除",
"plan.menu.roof.cover.roof.shape.edit": "屋根形状編集",
"plan.menu.roof.cover.auxiliary.line.drawing": "補助線の作成",
"modal.cover.outline.drawing": "外壁線の作成",

View File

@ -57,6 +57,7 @@
"modal.movement.flow.line.top.right": "높이변경 : 위, 오른쪽",
"plan.menu.roof.cover.outline.edit.offset": "외벽선 편집 및 오프셋",
"plan.menu.roof.cover.roof.surface.alloc": "지붕면 할당",
"plan.menu.roof.cover.roof.surface.all.remove": "배치면 전체 삭제",
"plan.menu.roof.cover.roof.shape.edit": "지붕형상 편집",
"plan.menu.roof.cover.auxiliary.line.drawing": "보조선 작성",
"modal.cover.outline.drawing": "외벽선 작성",

View File

@ -23,9 +23,19 @@ export const menusState = atom({
},
{ type: 'outline', name: 'plan.menu.roof.cover', icon: 'con02', title: MENU.ROOF_COVERING.DEFAULT },
{ type: 'surface', name: 'plan.menu.placement.surface', icon: 'con03', title: MENU.BATCH_CANVAS.DEFAULT },
{ type: 'module', name: 'plan.menu.module.circuit.setting', icon: 'con04', title: MENU.MODULE_CIRCUIT_SETTING.DEFAULT },
{
type: 'module',
name: 'plan.menu.module.circuit.setting',
icon: 'con04',
title: MENU.MODULE_CIRCUIT_SETTING.DEFAULT,
},
{ type: 'estimate', name: 'plan.menu.estimate', icon: 'con06', title: MENU.ESTIMATE.DEFAULT },
{ type: 'simulation', name: 'plan.menu.simulation', icon: 'con05', title: MENU.POWER_GENERATION_SIMULATION.DEFAULT },
{
type: 'simulation',
name: 'plan.menu.simulation',
icon: 'con05',
title: MENU.POWER_GENERATION_SIMULATION.DEFAULT,
},
],
})
@ -37,16 +47,17 @@ export const subMenusState = atom({
// 지붕덮개
{ id: 0, name: 'plan.menu.roof.cover.outline.drawing', menu: MENU.ROOF_COVERING.EXTERIOR_WALL_LINE },
{ id: 1, name: 'plan.menu.roof.cover.roof.shape.setting', menu: MENU.ROOF_COVERING.ROOF_SHAPE_SETTINGS },
// {
// id: 2,
// name: 'plan.menu.roof.cover.roof.shape.passivity.setting',
// menu: MENU.ROOF_COVERING.ROOF_SHAPE_PASSIVITY_SETTINGS,
// },
{
id: 2,
name: 'plan.menu.roof.cover.roof.shape.passivity.setting',
menu: MENU.ROOF_COVERING.ROOF_SHAPE_PASSIVITY_SETTINGS,
},
{ id: 3, name: 'plan.menu.roof.cover.auxiliary.line.drawing', menu: MENU.ROOF_COVERING.HELP_LINE_DRAWING },
{ id: 4, name: 'plan.menu.roof.cover.eaves.kerava.edit', menu: MENU.ROOF_COVERING.EAVES_KERAVA_EDIT },
{ id: 5, name: 'plan.menu.roof.cover.movement.shape.updown', menu: MENU.ROOF_COVERING.MOVEMENT_SHAPE_UPDOWN },
{ id: 6, name: 'plan.menu.roof.cover.outline.edit.offset', menu: MENU.ROOF_COVERING.OUTLINE_EDIT_OFFSET },
{ id: 7, name: 'plan.menu.roof.cover.roof.surface.alloc', menu: MENU.ROOF_COVERING.ROOF_SHAPE_ALLOC },
{ id: 8, name: 'plan.menu.roof.cover.roof.surface.all.remove', menu: MENU.ROOF_COVERING.ALL_REMOVE },
],
surface: [
// 배치면

156
src/styles/calc.scss Normal file
View File

@ -0,0 +1,156 @@
// 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
);
// Mixins
@mixin button-styles {
padding: 0.125rem;
border-radius: 0.5rem;
font-weight: bold;
font-size: 0.625rem;
color: white;
border: none;
cursor: pointer;
transition: all 0.1s ease-in-out;
&:active {
transform: scale(0.95);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
}
// Calculator Input Wrapper
.calculator-input-wrapper {
position: relative;
width: 100%;
display: inline-block;
// Input Field
.calculator-input {
width: 100%;
padding: 0.5rem 1rem;
background-color: map-get($colors, 'dark-600');
border: 1px solid map-get($colors, 'border');
color: white;
font-weight: bold;
border-radius: 0.5rem;
text-align: right;
cursor: pointer;
font-size: 0.625rem;
box-sizing: border-box;
&:focus {
outline: none;
border-color: map-get($colors, 'primary');
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
}
}
// Keypad Container
.keypad-container {
position: absolute;
top: calc(100% + 2px);
left: 0;
right: 0;
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);
padding: 0.5rem;
z-index: 1000;
animation: fadeIn 0.15s ease-out;
border: 1px solid map-get($colors, 'border');
box-sizing: border-box;
// Keypad Grid
.keypad-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.125rem;
// Button Base
button {
@include button-styles;
}
// Button Types
.btn-number {
background-color: map-get($colors, 'dark-500');
&:hover {
background-color: map-get($colors, 'dark-400');
}
}
.btn-operator {
background-color: map-get($colors, 'warning');
&:hover {
background-color: map-get($colors, 'warning-dark');
}
}
.btn-clear {
background-color: map-get($colors, 'danger');
grid-column: span 2;
&:hover {
background-color: map-get($colors, 'danger-dark');
}
}
.btn-delete {
background-color: map-get($colors, 'primary');
grid-column: span 2;
&:hover {
background-color: map-get($colors, 'primary-dark');
}
}
.btn-equals {
background-color: map-get($colors, 'warning');
&:hover {
background-color: map-get($colors, 'warning-dark');
}
}
.btn-zero {
grid-column: span 1;
}
}
}
}
// Animations
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
// Responsive Design
@media (max-width: 640px) {
.keypad-grid button {
padding: 0.5rem;
font-size: 0.875rem;
}
}

172
src/util/calc-utils.js Normal file
View File

@ -0,0 +1,172 @@
export const createCalculator = (options = {}) => {
const state = {
currentOperand: '',
previousOperand: '',
operation: undefined,
shouldResetDisplay: false,
allowNegative: options.allowNegative ?? true,
allowDecimal: options.allowDecimal ?? true,
allowZero: options.allowZero ?? true,
decimalPlaces: options.decimalPlaces ?? null,
}
// Expose state for debugging and direct access
const getState = () => ({ ...state })
const clear = () => {
state.currentOperand = ''
state.previousOperand = ''
state.operation = undefined
state.shouldResetDisplay = false
return state.currentOperand
}
const deleteNumber = () => {
if (state.currentOperand.length <= 1) {
state.currentOperand = '0'
} else {
state.currentOperand = state.currentOperand.toString().slice(0, -1)
}
return state.currentOperand
}
const appendNumber = (number) => {
if (number === '.' && !state.allowDecimal) return state.currentOperand
if (number === '.' && state.currentOperand.includes('.')) return state.currentOperand
if (state.shouldResetDisplay) {
state.currentOperand = number.toString()
state.shouldResetDisplay = false
} else {
if (state.currentOperand === '0' && number !== '.') {
state.currentOperand = number.toString()
} else {
state.currentOperand = state.currentOperand.toString() + number.toString()
}
}
return state.currentOperand
}
const chooseOperation = (operation) => {
if (operation === '-' && state.currentOperand === '0' && state.previousOperand === '' && state.allowNegative) {
state.currentOperand = '-'
return state.currentOperand
}
if (state.currentOperand === '' || state.currentOperand === '-') return state.currentOperand
// If there's a previous operation, compute it first
if (state.previousOperand !== '') {
compute()
}
state.operation = operation
state.previousOperand = state.currentOperand
state.currentOperand = ''
return state.previousOperand + state.operation
}
const compute = () => {
// If there's no operation, return the current value
if (!state.operation) return parseFloat(state.currentOperand || '0')
// If there's no current operand but we have a previous one, use previous as current
if (state.currentOperand === '' && state.previousOperand !== '') {
state.currentOperand = state.previousOperand
}
const prev = parseFloat(state.previousOperand)
const current = parseFloat(state.currentOperand)
if (isNaN(prev) || isNaN(current)) return 0
let result
switch (state.operation) {
case '+':
result = prev + current
break
case '-':
result = prev - current
break
case '×':
result = prev * current
break
case '÷':
if (current === 0) {
state.currentOperand = 'Error'
return 0
}
result = prev / current
break
default:
return parseFloat(state.currentOperand || '0')
}
// Apply formatting and constraints
if (state.decimalPlaces !== null) {
result = Number(result.toFixed(state.decimalPlaces))
}
if (!state.allowDecimal) {
result = Math.round(result)
}
if (!state.allowNegative && result < 0) {
result = 0
}
if (!state.allowZero && result === 0) {
result = 1
}
// Update state
state.currentOperand = result.toString()
state.previousOperand = ''
state.operation = undefined
state.shouldResetDisplay = true
return result
}
// Getter methods for the calculator state
const getCurrentOperand = () => state.currentOperand
const getPreviousOperand = () => state.previousOperand
const getOperation = () => state.operation
const getDisplayValue = () => {
if (state.operation && state.previousOperand) {
return `${state.previousOperand} ${state.operation} ${state.currentOperand || ''}`.trim()
}
return state.currentOperand
}
return {
// Core calculator methods
clear,
delete: deleteNumber, // Alias for deleteNumber for compatibility
deleteNumber,
appendNumber,
chooseOperation,
compute,
// State getters
getDisplayValue,
getCurrentOperand,
getPreviousOperand,
getOperation,
// Direct state access (for debugging)
getState,
// Direct property access (for compatibility with CalcInput.jsx)
get currentOperand() { return state.currentOperand },
get previousOperand() { return state.previousOperand },
get operation() { return state.operation },
get shouldResetDisplay() { return state.shouldResetDisplay },
// Setter for direct property access (if needed)
set currentOperand(value) { state.currentOperand = value },
set previousOperand(value) { state.previousOperand = value },
set operation(value) { state.operation = value },
set shouldResetDisplay(value) { state.shouldResetDisplay = value }
}
}

39
tsconfig.json Normal file
View File

@ -0,0 +1,39 @@
{
"compilerOptions": {
"target": "es2015",
"downlevelIteration": true,
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"noEmit": true,
"incremental": true,
"module": "esnext",
"esModuleInterop": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
".next/types/**/*.ts",
"**/*.ts",
"**/*.tsx"
],
"exclude": [
"node_modules"
]
}