dev #784

Merged
ysCha merged 3 commits from dev into dev-deploy 2026-04-16 13:21:25 +09:00
2 changed files with 302 additions and 59 deletions

View File

@ -134,9 +134,7 @@ export default function StepUp(props) {
const moduleIds = targetSurface.modules.map((module) => module.id) const moduleIds = targetSurface.modules.map((module) => module.id)
/** 기존 모듈 텍스트 삭제 */ /** 기존 모듈 텍스트 삭제 */
canvas ;[...canvas.getObjects().filter((obj) => moduleIds.includes(obj.parentId))]
.getObjects()
.filter((obj) => moduleIds.includes(obj.parentId))
.forEach((text) => canvas.remove(text)) .forEach((text) => canvas.remove(text))
/** 새로운 모듈 회로 정보 추가 */ /** 새로운 모듈 회로 정보 추가 */
@ -410,33 +408,10 @@ export default function StepUp(props) {
* 주어진 stepUpListData 기준으로 캔버스의 모든 module circuit 텍스트를 다시 그린다. * 주어진 stepUpListData 기준으로 캔버스의 모든 module circuit 텍스트를 다시 그린다.
* 모든 pcsItem selected serQty 순회해 적용한다. * 모든 pcsItem selected serQty 순회해 적용한다.
*/ */
const applyCircuitsToCanvas = (stepUpListSrc) => { /**
if (!stepUpListSrc?.[0]?.pcsItemList) return * 모듈에 circuit 텍스트를 추가하는 공통 함수
*/
/** 모든 모듈 circuit 데이터 초기화 */ const addCircuitTextToModule = (module) => {
canvas
.getObjects()
.filter((obj) => obj.name === POLYGON_TYPE.MODULE)
.forEach((module) => {
module.circuit = null
module.circuitNumber = null
module.pcsItemId = null
})
/** 기존 circuit 텍스트 객체 모두 제거 */
canvas
.getObjects()
.filter((obj) => obj.name === 'circuitNumber')
.forEach((text) => canvas.remove(text))
stepUpListSrc[0].pcsItemList.forEach((pcsItem) => {
const sel = pcsItem.serQtyList?.find((sq) => sq.selected)
if (!sel) return
sel.roofSurfaceList.forEach((roofSurface) => {
const targetSurface = canvas.getObjects().filter((obj) => obj.id === roofSurface.roofSurfaceId)[0]
if (!targetSurface) return
roofSurface.moduleList.forEach((module) => {
const targetModule = canvas.getObjects().find((obj) => obj.id === module.uniqueId) const targetModule = canvas.getObjects().find((obj) => obj.id === module.uniqueId)
if (targetModule && module.circuit !== '' && module.circuit !== null) { if (targetModule && module.circuit !== '' && module.circuit !== null) {
const moduleCircuitText = new fabric.Text(module.circuit, { const moduleCircuitText = new fabric.Text(module.circuit, {
@ -462,9 +437,71 @@ export default function StepUp(props) {
targetModule.circuitNumber = module.circuit targetModule.circuitNumber = module.circuit
canvas.add(moduleCircuitText) canvas.add(moduleCircuitText)
} }
}
const applyCircuitsToCanvas = (stepUpListSrc, mainIdx, subIdx) => {
if (!stepUpListSrc?.[0]?.pcsItemList) return
if (mainIdx >= 1) {
/**
* 우측 PCS (mainIdx >= 1) 클릭 :
* 해당 pcsItem selected serQty 찾아서 moduleList circuit 갱신한다.
* 좌측 PCS circuit 그대로 유지.
*/
const pcsItem = stepUpListSrc[0].pcsItemList[mainIdx]
const sel = pcsItem?.serQtyList?.find((sq) => sq.selected)
if (!sel) return
// pcsItem moduleList circuit
const moduleUniqueIds = new Set()
sel.roofSurfaceList.forEach((rs) => {
rs.moduleList.forEach((m) => moduleUniqueIds.add(m.uniqueId))
})
// circuit +
const targetModuleObjs = canvas.getObjects().filter(
(obj) => obj.name === POLYGON_TYPE.MODULE && moduleUniqueIds.has(obj.id),
)
targetModuleObjs.forEach((module) => {
module.circuit = null
module.circuitNumber = null
module.pcsItemId = null
})
const textsToRemove = [...canvas.getObjects().filter(
(obj) => obj.name === 'circuitNumber' && moduleUniqueIds.has(obj.parentId),
)]
textsToRemove.forEach((text) => canvas.remove(text))
// circuit
sel.roofSurfaceList.forEach((roofSurface) => {
roofSurface.moduleList.forEach((module) => addCircuitTextToModule(module))
})
} else {
/**
* 좌측 PCS (mainIdx === 0) 클릭 :
* 전체 모듈 circuit 초기화 모든 pcsItem selected 적용
*/
canvas
.getObjects()
.filter((obj) => obj.name === POLYGON_TYPE.MODULE)
.forEach((module) => {
module.circuit = null
module.circuitNumber = null
module.pcsItemId = null
})
const circuitTexts = [...canvas.getObjects().filter((obj) => obj.name === 'circuitNumber')]
circuitTexts.forEach((text) => canvas.remove(text))
stepUpListSrc[0].pcsItemList.forEach((pcsItem) => {
const sel = pcsItem.serQtyList?.find((sq) => sq.selected)
if (!sel) return
sel.roofSurfaceList.forEach((roofSurface) => {
roofSurface.moduleList.forEach((module) => addCircuitTextToModule(module))
}) })
}) })
}) }
canvas.renderAll() canvas.renderAll()
setModuleStatisticsData() setModuleStatisticsData()
@ -483,8 +520,7 @@ export default function StepUp(props) {
setStepUpListData(tempStepUpListData) setStepUpListData(tempStepUpListData)
/** PCS 2개 이상 또는 첫번째 PCS 선택 시에만 실행 */ /** PCS 2개 이상 또는 첫번째 PCS 선택 시에만 실행 */
const needsRefetch = const needsRefetch = tempStepUpListData[0].pcsItemList.length > 1 && mainIdx === 0
tempStepUpListData[0].pcsItemList.length > 1 && mainIdx === 0
if (needsRefetch) { if (needsRefetch) {
/** 파워컨디셔너 옵션 조회 요청 파라미터 */ /** 파워컨디셔너 옵션 조회 요청 파라미터 */
@ -522,10 +558,10 @@ export default function StepUp(props) {
const dataArray = Array.isArray(res.data) ? res.data : [res.data] const dataArray = Array.isArray(res.data) ? res.data : [res.data]
const newStepUpListData = formatStepUpListData(dataArray) const newStepUpListData = formatStepUpListData(dataArray)
setStepUpListData(newStepUpListData) setStepUpListData(newStepUpListData)
applyCircuitsToCanvas(newStepUpListData) applyCircuitsToCanvas(newStepUpListData, mainIdx, subIdx)
} else { } else {
swalFire({ text: getMessage('common.message.send.error') }) swalFire({ text: getMessage('common.message.send.error') })
applyCircuitsToCanvas(tempStepUpListData) applyCircuitsToCanvas(tempStepUpListData, mainIdx, subIdx)
} }
}) })
return return
@ -533,7 +569,7 @@ export default function StepUp(props) {
} }
/** API 재조회가 없는 경로: 현재 tempStepUpListData 로 즉시 캔버스 갱신 */ /** API 재조회가 없는 경로: 현재 tempStepUpListData 로 즉시 캔버스 갱신 */
applyCircuitsToCanvas(tempStepUpListData) applyCircuitsToCanvas(tempStepUpListData, mainIdx, subIdx)
} }
/** /**
@ -551,7 +587,17 @@ export default function StepUp(props) {
paralQty: serQty.paralQty, paralQty: serQty.paralQty,
// : itemId 1, // : itemId 1,
connections: item.connList?.length connections: item.connList?.length
? [...new Map(item.connList.map((conn) => [conn.itemId, { connItemId: conn.itemId, connMaxParalCnt: conn.connMaxParalCnt }])).values()] ? [
...new Map(
item.connList.map((conn) => [
conn.itemId,
{
connItemId: conn.itemId,
connMaxParalCnt: conn.connMaxParalCnt,
},
]),
).values(),
]
: [], : [],
} }
}) })

View File

@ -559,21 +559,108 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
console.log('[DEBUG] changRoofLinePoints(최종 SK입력):', changRoofLinePoints.map((p, i) => `[${i}] (${Math.round(p.x)},${Math.round(p.y)})`)) console.log('[DEBUG] changRoofLinePoints(최종 SK입력):', changRoofLinePoints.map((p, i) => `[${i}] (${Math.round(p.x)},${Math.round(p.y)})`))
// 좌표 유효성 검증
const invalidPoints = changRoofLinePoints.filter((p, i) =>
p == null || isNaN(p.x) || isNaN(p.y) || !isFinite(p.x) || !isFinite(p.y)
)
if (invalidPoints.length > 0) {
console.error('[SK_ERROR] 유효하지 않은 좌표 발견:', invalidPoints)
console.error('[SK_ERROR] 전체 좌표:', JSON.stringify(changRoofLinePoints))
}
// 인접 중복점 검출 (거리 < 0.5)
for (let i = 0; i < changRoofLinePoints.length; i++) {
const curr = changRoofLinePoints[i]
const next = changRoofLinePoints[(i + 1) % changRoofLinePoints.length]
const dist = Math.hypot(next.x - curr.x, next.y - curr.y)
if (dist < 0.5) {
console.warn(`[SK_WARN] 인접 중복점: [${i}](${Math.round(curr.x)},${Math.round(curr.y)}) ↔ [${(i + 1) % changRoofLinePoints.length}](${Math.round(next.x)},${Math.round(next.y)}) dist=${dist.toFixed(3)}`)
}
}
// 일직선(collinear) 점 검출
for (let i = 0; i < changRoofLinePoints.length; i++) {
const prev = changRoofLinePoints[(i - 1 + changRoofLinePoints.length) % changRoofLinePoints.length]
const curr = changRoofLinePoints[i]
const next = changRoofLinePoints[(i + 1) % changRoofLinePoints.length]
const cross = (curr.x - prev.x) * (next.y - prev.y) - (curr.y - prev.y) * (next.x - prev.x)
if (Math.abs(cross) < 1.0) {
console.warn(`[SK_WARN] 일직선 점: [${i}](${Math.round(curr.x)},${Math.round(curr.y)}) cross=${cross.toFixed(3)}`)
}
}
// 다각형 오버 검출: 원본 roof.points와 비교하여 꼭짓점 방향(cross product)이 뒤집혔는지 체크
let isOverDetected = false
const origPoints = roof.points || []
const n = changRoofLinePoints.length
console.log('[SK_OVER_DEBUG] origPoints:', origPoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`))
console.log('[SK_OVER_DEBUG] changRoofLinePoints:', changRoofLinePoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`))
console.log('[SK_OVER_DEBUG] 변경된 점:', changRoofLinePoints.map((p, i) => {
const o = origPoints[i]
if (!o) return null
const dist = Math.hypot(p.x - o.x, p.y - o.y)
return dist > 1 ? `[${i}] dx=${Math.round(p.x - o.x)} dy=${Math.round(p.y - o.y)}` : null
}).filter(Boolean))
if (origPoints.length === n && n >= 3) {
for (let i = 0; i < n; i++) {
const prevOrig = origPoints[(i - 1 + n) % n]
const currOrig = origPoints[i]
const nextOrig = origPoints[(i + 1) % n]
const crossOrig = (currOrig.x - prevOrig.x) * (nextOrig.y - prevOrig.y) - (currOrig.y - prevOrig.y) * (nextOrig.x - prevOrig.x)
const prevNew = changRoofLinePoints[(i - 1 + n) % n]
const currNew = changRoofLinePoints[i]
const nextNew = changRoofLinePoints[(i + 1) % n]
const crossNew = (currNew.x - prevNew.x) * (nextNew.y - prevNew.y) - (currNew.y - prevNew.y) * (nextNew.x - prevNew.x)
if (crossOrig * crossNew < 0) {
isOverDetected = true
console.error(`[SK_OVER] 꼭짓점[${i}] 방향 뒤집힘! cross: ${crossOrig.toFixed(1)}${crossNew.toFixed(1)}`)
console.error(`[SK_OVER] 원본: [${(i-1+n)%n}](${Math.round(prevOrig.x)},${Math.round(prevOrig.y)}) → [${i}](${Math.round(currOrig.x)},${Math.round(currOrig.y)}) → [${(i+1)%n}](${Math.round(nextOrig.x)},${Math.round(nextOrig.y)})`)
console.error(`[SK_OVER] 변경: [${(i-1+n)%n}](${Math.round(prevNew.x)},${Math.round(prevNew.y)}) → [${i}](${Math.round(currNew.x)},${Math.round(currNew.y)}) → [${(i+1)%n}](${Math.round(nextNew.x)},${Math.round(nextNew.y)})`)
}
}
}
// changRoofLinePoints를 roof.skeletonPoints에 저장 (roof.points 원본은 유지) // changRoofLinePoints를 roof.skeletonPoints에 저장 (roof.points 원본은 유지)
roof.skeletonPoints = changRoofLinePoints.map(p => ({ x: p.x, y: p.y })) roof.skeletonPoints = changRoofLinePoints.map(p => ({ x: p.x, y: p.y }))
const perturbedPoints = perturbPolygonPoints(changRoofLinePoints) // ──────────────────────────────────────────────────────────────────
// [분기] 스켈레톤 입력 포인트 결정
// 정상: changRoofLinePoints 그대로 사용 (기존 로직 변경 없음)
// 오버: calcOverCorrectedPoints() 별도 함수로 보정 포인트 생성
// ※ changRoofLinePoints 자체는 절대 수정하지 않음
// ※ 오버 보정 실패 시 changRoofLinePoints 그대로 fallback
// ──────────────────────────────────────────────────────────────────
let skeletonInputPoints = changRoofLinePoints // 정상 경로
if (isOverDetected) {
// [예외] 오버 감지 시 → 별도 함수에서 스켈레톤 전용 보정 포인트 계산
try {
const corrected = calcOverCorrectedPoints(changRoofLinePoints, origPoints)
if (corrected && corrected.length >= 3) {
skeletonInputPoints = corrected
console.log('[SK_OVER] 보정 포인트 적용:', skeletonInputPoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`))
}
} catch (e) {
// 보정 실패 시 기존 changRoofLinePoints로 fallback → 기존 동작 보장
console.error('[SK_OVER] 보정 실패 → fallback:', e)
}
}
const perturbedPoints = perturbPolygonPoints(skeletonInputPoints)
const geoJSONPolygon = toGeoJSON(perturbedPoints) const geoJSONPolygon = toGeoJSON(perturbedPoints)
try { try {
// SkeletonBuilder는 닫히지 않은 폴리곤을 기대하므로 마지막 점 제거 // SkeletonBuilder는 닫히지 않은 폴리곤을 기대하므로 마지막 점 제거
geoJSONPolygon.pop() geoJSONPolygon.pop()
console.log('[SkeletonBuilder] geoJSONPolygon:', JSON.stringify(geoJSONPolygon, null, 2)) console.log('[SkeletonBuilder] geoJSONPolygon:', JSON.stringify(geoJSONPolygon, null, 2))
console.log('[SkeletonBuilder] 꼭짓점 수:', geoJSONPolygon.length)
const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]]) const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]])
// 스켈레톤 데이터를 기반으로 내부선 생성 // 스켈레톤 데이터를 기반으로 내부선 생성
roof.innerLines = roof.innerLines || [] roof.innerLines = roof.innerLines || []
roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode) roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode, isOverDetected)
//console.log("roofInnerLines:::", roof.innerLines); //console.log("roofInnerLines:::", roof.innerLines);
// 캔버스에 스켈레톤 상태 저장 // 캔버스에 스켈레톤 상태 저장
if (!canvas.skeletonStates) { if (!canvas.skeletonStates) {
@ -606,7 +693,9 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
//console.log('skeleton rendered.', canvas) //console.log('skeleton rendered.', canvas)
} catch (e) { } catch (e) {
console.error('스켈레톤 생성 중 오류 발생:', e) console.error('[SK_ERROR] 스켈레톤 생성 중 오류 발생:', e)
console.error('[SK_ERROR] 입력 좌표:', changRoofLinePoints.map((p, i) => `[${i}] (${p.x.toFixed(2)},${p.y.toFixed(2)})`))
console.error('[SK_ERROR] geoJSON:', JSON.stringify(geoJSONPolygon))
if (canvas.skeletonStates) { if (canvas.skeletonStates) {
canvas.skeletonStates[roofId] = false canvas.skeletonStates[roofId] = false
canvas.skeletonStates = {} canvas.skeletonStates = {}
@ -624,7 +713,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
* @param {string} textMode - 텍스트 표시 모드 ('plane', 'actual', 'none') * @param {string} textMode - 텍스트 표시 모드 ('plane', 'actual', 'none')
* @param {Array<QLine>} baseLines - 원본 외벽선 QLine 객체 배열 * @param {Array<QLine>} baseLines - 원본 외벽선 QLine 객체 배열
*/ */
const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => { const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOverDetected = false) => {
if (!skeleton?.Edges) return [] if (!skeleton?.Edges) return []
const roof = canvas?.getObjects().find((object) => object.id === roofId) const roof = canvas?.getObjects().find((object) => object.id === roofId)
const wall = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.WALL && obj.attributes.roofId === roofId) const wall = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.WALL && obj.attributes.roofId === roofId)
@ -1083,8 +1172,6 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
return return
} }
const movedStart = Math.abs(wallBaseLine.x1 - wallLine.x1) > EPSILON || Math.abs(wallBaseLine.y1 - wallLine.y1) > EPSILON const movedStart = Math.abs(wallBaseLine.x1 - wallLine.x1) > EPSILON || Math.abs(wallBaseLine.y1 - wallLine.y1) > EPSILON
const movedEnd = Math.abs(wallBaseLine.x2 - wallLine.x2) > EPSILON || Math.abs(wallBaseLine.y2 - wallLine.y2) > EPSILON const movedEnd = Math.abs(wallBaseLine.x2 - wallLine.x2) > EPSILON || Math.abs(wallBaseLine.y2 - wallLine.y2) > EPSILON
@ -1684,7 +1771,9 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode) => {
canvas.renderAll() canvas.renderAll()
}) })
} }
if (!isOverDetected) {
getMoveUpDownLine() getMoveUpDownLine()
}
} }
@ -2000,6 +2089,114 @@ function processGableEdge(edgeResult, baseLines, skeletonLines, selectBaseLine,
// --- Helper Functions --- // --- Helper Functions ---
/**
* [오버 전용 예외 처리 함수] calcOverCorrectedPoints
*
* wallLine이 인접 roofLine을 넘어섰을 (오버 감지 )에만 호출된다.
* changRoofLinePoints를 직접 수정하지 않고 스켈레톤 전용 보정 포인트를 새로 만들어 반환한다.
*
* 보정 방법:
* 1. 이동된 (moved) 판별
* 2. 이동점이 인접 고정점을 넘어갔으면 이동점을 고정점 위치로 클램핑
* 3. 클램핑된 점에서 연결된 이동점으로 전파
* 4. 인접 중복점 제거 (dist < 0.5)
* 5. 일직선(collinear) 중간점 제거
*
* @param {Array<{x,y}>} points - changRoofLinePoints (수정하지 않음, 읽기 전용)
* @param {Array<{x,y}>} origPoints - roof.points (원본 폴리곤)
* @returns {Array<{x,y}>} 보정된 스켈레톤 입력 포인트 (실패 원본 반환)
*/
const calcOverCorrectedPoints = (points, origPoints) => {
const n = points.length
const skPoints = points.map(p => ({ x: p.x, y: p.y }))
// Step 1. 이동된 점 판별 (원본 대비 1px 초과 이동)
const moved = skPoints.map((p, i) => Math.hypot(p.x - origPoints[i].x, p.y - origPoints[i].y) > 1)
// Step 2. 이동점이 인접 고정점을 넘어갔으면 → 고정점 위치로 클램핑
const clamped = new Set()
const clampToFixed = (movedIdx, fixedIdx) => {
let didClamp = false
// 원래 수평 연결 (같은 y) → x 클램핑
if (Math.abs(origPoints[movedIdx].y - origPoints[fixedIdx].y) < 1) {
const origDir = origPoints[movedIdx].x - origPoints[fixedIdx].x
const newDir = skPoints[movedIdx].x - skPoints[fixedIdx].x
if (origDir * newDir < 0) {
console.log(`[SK_OVER] 클램핑: [${movedIdx}].x ${Math.round(skPoints[movedIdx].x)}${Math.round(skPoints[fixedIdx].x)}`)
skPoints[movedIdx].x = skPoints[fixedIdx].x
didClamp = true
}
}
// 원래 수직 연결 (같은 x) → y 클램핑
if (Math.abs(origPoints[movedIdx].x - origPoints[fixedIdx].x) < 1) {
const origDir = origPoints[movedIdx].y - origPoints[fixedIdx].y
const newDir = skPoints[movedIdx].y - skPoints[fixedIdx].y
if (origDir * newDir < 0) {
console.log(`[SK_OVER] 클램핑: [${movedIdx}].y ${Math.round(skPoints[movedIdx].y)}${Math.round(skPoints[fixedIdx].y)}`)
skPoints[movedIdx].y = skPoints[fixedIdx].y
didClamp = true
}
}
if (didClamp) clamped.add(movedIdx)
}
for (let i = 0; i < n; i++) {
if (!moved[i]) continue
const nextIdx = (i + 1) % n
const prevIdx = (i - 1 + n) % n
if (!moved[nextIdx]) clampToFixed(i, nextIdx)
if (!moved[prevIdx]) clampToFixed(i, prevIdx)
}
// Step 3. 클램핑된 점 → 연결된 미클램핑 이동점으로 값 전파 (반복)
let propagated = true
while (propagated) {
propagated = false
for (let i = 0; i < n; i++) {
if (!moved[i] || clamped.has(i)) continue
const prevIdx = (i - 1 + n) % n
const nextIdx = (i + 1) % n
if (clamped.has(prevIdx) && Math.abs(origPoints[prevIdx].x - origPoints[i].x) < 1) {
skPoints[i].x = skPoints[prevIdx].x; clamped.add(i); propagated = true
}
if (clamped.has(prevIdx) && Math.abs(origPoints[prevIdx].y - origPoints[i].y) < 1) {
skPoints[i].y = skPoints[prevIdx].y; clamped.add(i); propagated = true
}
if (clamped.has(nextIdx) && Math.abs(origPoints[nextIdx].x - origPoints[i].x) < 1) {
skPoints[i].x = skPoints[nextIdx].x; clamped.add(i); propagated = true
}
if (clamped.has(nextIdx) && Math.abs(origPoints[nextIdx].y - origPoints[i].y) < 1) {
skPoints[i].y = skPoints[nextIdx].y; clamped.add(i); propagated = true
}
}
}
// Step 4. 인접 중복점 제거 (dist < 0.5)
const deduped = []
for (let i = 0; i < skPoints.length; i++) {
const next = skPoints[(i + 1) % skPoints.length]
if (Math.hypot(next.x - skPoints[i].x, next.y - skPoints[i].y) > 0.5) {
deduped.push(skPoints[i])
}
}
// Step 5. 일직선(collinear) 중간점 제거
const cleaned = []
for (let i = 0; i < deduped.length; i++) {
const prev = deduped[(i - 1 + deduped.length) % deduped.length]
const curr = deduped[i]
const next = deduped[(i + 1) % deduped.length]
const cross = (curr.x - prev.x) * (next.y - prev.y) - (curr.y - prev.y) * (next.x - prev.x)
if (Math.abs(cross) > 1.0) cleaned.push(curr)
}
// 보정 실패 시(꼭짓점 부족) 원본 반환 → 기존 동작 보장
if (cleaned.length >= 3) return cleaned
if (deduped.length >= 3) return deduped
console.warn('[SK_OVER] calcOverCorrectedPoints: 보정 실패 → 원본 반환')
return points
}
/** /**
* 점으로 이루어진 선분이 외벽선인지 확인합니다. * 점으로 이루어진 선분이 외벽선인지 확인합니다.
* @param {object} p1 - 점1 {x, y} * @param {object} p1 - 점1 {x, y}