잔여선 삭제 및 라벨표시(local) #796
@ -8,6 +8,86 @@ import { LINE_TYPE, POLYGON_TYPE } from '@/common/common'
|
||||
import Big from 'big.js'
|
||||
import { drawSkeletonRidgeRoof, drawSkeletonRidgeRoofFromBaseLines, verifyMoveBoundary } from '@/util/skeleton-utils'
|
||||
|
||||
// ========================================================================
|
||||
// [DEBUG] 캔버스 라인 라벨 — 로컬 전용 (NEXT_PUBLIC_RUN_MODE === 'local')
|
||||
// ----------------------------------------------------------------------
|
||||
// 별도 util 파일을 두지 않고 QPolygon.js 안에 인라인.
|
||||
// dev/prd 에선 가드가 즉시 false → 코드 미실행. 외부 의존성 없음.
|
||||
// 라인 자체의 stroke/fill 은 절대 손대지 않고 fabric.Text 만 추가.
|
||||
// ========================================================================
|
||||
const DEBUG_LABEL_NAME = '__debugLabel'
|
||||
|
||||
function __isDebugLabelsEnabled() {
|
||||
return process.env.NEXT_PUBLIC_RUN_MODE === 'local'
|
||||
}
|
||||
|
||||
function __classifyLineForLabel(obj) {
|
||||
const name = obj.name
|
||||
const lineName = obj.lineName
|
||||
const type = obj.attributes?.type
|
||||
|
||||
if (name === 'baseLine') return 'B'
|
||||
if (name === 'outerLine' || name === 'eaves' || lineName === 'roofLine') return 'R'
|
||||
if (name === LINE_TYPE.SUBLINE.HIP || lineName === LINE_TYPE.SUBLINE.HIP) return 'H'
|
||||
if (name === LINE_TYPE.SUBLINE.RIDGE || lineName === LINE_TYPE.SUBLINE.RIDGE) return 'RG'
|
||||
if (name === LINE_TYPE.SUBLINE.VALLEY || lineName === LINE_TYPE.SUBLINE.VALLEY) return 'V'
|
||||
if (name === LINE_TYPE.SUBLINE.GABLE || lineName === LINE_TYPE.SUBLINE.GABLE) return 'G'
|
||||
if (name === LINE_TYPE.SUBLINE.VERGE || lineName === LINE_TYPE.SUBLINE.VERGE) return 'VG'
|
||||
if (
|
||||
type === LINE_TYPE.WALLLINE.EAVE_HELP_LINE ||
|
||||
lineName === LINE_TYPE.WALLLINE.EAVE_HELP_LINE ||
|
||||
name === LINE_TYPE.WALLLINE.EAVE_HELP_LINE
|
||||
) return 'E'
|
||||
if (
|
||||
typeof obj.x1 === 'number' &&
|
||||
typeof obj.y1 === 'number' &&
|
||||
typeof obj.x2 === 'number' &&
|
||||
typeof obj.y2 === 'number'
|
||||
) return 'SK'
|
||||
return null
|
||||
}
|
||||
|
||||
function __attachDebugLabels(canvas, parentId) {
|
||||
if (!__isDebugLabelsEnabled()) return
|
||||
if (!canvas || !parentId) return
|
||||
|
||||
const counters = {}
|
||||
const objects = canvas.getObjects().filter((o) => o.parentId === parentId && o.name !== DEBUG_LABEL_NAME)
|
||||
|
||||
objects.forEach((obj) => {
|
||||
const prefix = __classifyLineForLabel(obj)
|
||||
if (!prefix) return
|
||||
|
||||
counters[prefix] = (counters[prefix] || 0) + 1
|
||||
const label = `${prefix}-${counters[prefix]}`
|
||||
const mx = (obj.x1 + obj.x2) / 2
|
||||
const my = (obj.y1 + obj.y2) / 2
|
||||
|
||||
const text = new fabric.Text(label, {
|
||||
left: mx,
|
||||
top: my,
|
||||
originX: 'center',
|
||||
originY: 'center',
|
||||
fontSize: 12,
|
||||
fill: '#000',
|
||||
fontFamily: 'monospace',
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: 'rgba(255,255,0,0.85)',
|
||||
selectable: false,
|
||||
evented: false,
|
||||
hasControls: false,
|
||||
hasBorders: false,
|
||||
name: DEBUG_LABEL_NAME,
|
||||
parentId: parentId,
|
||||
excludeFromExport: true,
|
||||
})
|
||||
canvas.add(text)
|
||||
text.bringToFront()
|
||||
})
|
||||
|
||||
canvas.renderAll()
|
||||
}
|
||||
|
||||
|
||||
export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
type: 'QPolygon',
|
||||
@ -515,6 +595,9 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
// console.log('변별로 설정')
|
||||
drawRoofByAttribute(this.id, this.canvas, textMode)
|
||||
}
|
||||
|
||||
// [DEBUG] 로컬(NEXT_PUBLIC_RUN_MODE==='local') 에서만 라벨 부착. 그 외 즉시 return.
|
||||
__attachDebugLabels(this.canvas, this.id)
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@ -940,7 +940,7 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
|
||||
|
||||
// 스켈레톤 데이터를 기반으로 내부선 생성
|
||||
roof.innerLines = roof.innerLines || []
|
||||
roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode, isOverDetected)
|
||||
roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode, isOverDetected, changRoofLinePoints)
|
||||
//console.log("roofInnerLines:::", roof.innerLines);
|
||||
// 캔버스에 스켈레톤 상태 저장
|
||||
if (!canvas.skeletonStates) {
|
||||
@ -1067,7 +1067,7 @@ export const drawSkeletonRidgeRoofFromBaseLines = (roofId, canvas, textMode) =>
|
||||
const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]])
|
||||
|
||||
roof.innerLines = roof.innerLines || []
|
||||
roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode, /* isOverDetected */ true)
|
||||
roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode, /* isOverDetected */ true, rawPoints)
|
||||
|
||||
if (!canvas.skeletonStates) {
|
||||
canvas.skeletonStates = {}
|
||||
@ -1111,7 +1111,7 @@ export const drawSkeletonRidgeRoofFromBaseLines = (roofId, canvas, textMode) =>
|
||||
* @param {string} textMode - 텍스트 표시 모드 ('plane', 'actual', 'none')
|
||||
* @param {Array<QLine>} baseLines - 원본 외벽선 QLine 객체 배열
|
||||
*/
|
||||
const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOverDetected = false) => {
|
||||
const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOverDetected = false, skPolygonPoints = []) => {
|
||||
if (!skeleton?.Edges) return []
|
||||
const roof = canvas?.getObjects().find((object) => object.id === roofId)
|
||||
const wall = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.WALL && obj.attributes.roofId === roofId)
|
||||
@ -1467,13 +1467,15 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver
|
||||
// [SHOULDER-ADJACENT] 인접 pair 가 SHOULDER_ABSORBED 된 경우 HIP 빗변은 orphan 이 됨.
|
||||
// 이유: processInBoth 는 roofLine(원본 roof.points 기반) 꼭짓점을 HIP 끝점으로 씀.
|
||||
// 인접 pair 흡수 = 해당 꼭짓점이 현재 wallBaseLine 토폴로지에 없음 → 공중에 뜬 대각선.
|
||||
// SHOULDER_ABSORBED 가드(line 1482 인근)와 같은 원인·패턴의 후속 증상.
|
||||
// SHOULDER_ABSORBED 가드(line 1608 인근)와 같은 원인·패턴의 후속 증상.
|
||||
// 임계 = 10: SHOULDER_ABSORBED skip 임계와 동일하게 맞춤. (OVER_EPS=0.5 → planeSize=5)
|
||||
// <1 로 두면 pair#3 흡수(planeSize=5) 인접인 pair#4 가 가드 통과 → phantom HIP 생성.
|
||||
{
|
||||
const n = sortWallBaseLines.length
|
||||
const prevBL = sortWallBaseLines[(index - 1 + n) % n]
|
||||
const nextBL = sortWallBaseLines[(index + 1) % n]
|
||||
const prevAbsorbed = (prevBL?.attributes?.planeSize ?? Infinity) < 1
|
||||
const nextAbsorbed = (nextBL?.attributes?.planeSize ?? Infinity) < 1
|
||||
const prevAbsorbed = (prevBL?.attributes?.planeSize ?? Infinity) < 10
|
||||
const nextAbsorbed = (nextBL?.attributes?.planeSize ?? Infinity) < 10
|
||||
if (prevAbsorbed || nextAbsorbed) {
|
||||
console.log(`⏭️ [pair#${index}] ${condition} HIP_SKIP adj_absorbed prev=${prevAbsorbed} next=${nextAbsorbed}`)
|
||||
return
|
||||
@ -1512,25 +1514,45 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver
|
||||
innerLines.push(createdLine)
|
||||
}
|
||||
|
||||
// [SK-VERTEX-PROXIMITY] HIP target 은 새 SK 폴리곤(changRoofLinePoints) 의 vertex 근처여야 함.
|
||||
// roofLine 은 OLD roof.points 기반 → 인접 흡수로 코너가 dissolve 되면 target 이 SK 어디에도 없는 phantom 좌표가 됨.
|
||||
// pair#0 의 prev/next 인접이 아닌 2-hop 흡수 (ex. left_out 흡수 후 인접 pair#7 stretched, pair#0 end 가 옛 코너 참조) 도 차단.
|
||||
const __isNearSkVertex = (pt, tol = 5) => {
|
||||
if (!Array.isArray(skPolygonPoints) || skPolygonPoints.length === 0) return true
|
||||
return skPolygonPoints.some((v) => Math.hypot(v.x - pt.x, v.y - pt.y) < tol)
|
||||
}
|
||||
|
||||
if (checkDist1 > 0) {
|
||||
// findPoints 불필요: wallBaseLine 좌표가 ridge 라인 위에 없음 (스켈레톤이 이동된 폴리곤 기준으로 재계산되므로)
|
||||
// HIP extension 라인만 생성
|
||||
let target
|
||||
if (moveDistY1 > moveDistX1) {
|
||||
const dist = moveDistY1 - moveDistX1
|
||||
createHipLine(sPoint, { x: roofLine.x1, y: roofLine.y1 + ySignStart * dist })
|
||||
target = { x: roofLine.x1, y: roofLine.y1 + ySignStart * dist }
|
||||
} else {
|
||||
const dist = moveDistX1 - moveDistY1
|
||||
createHipLine(sPoint, { x: roofLine.x1 + xSignStart * dist, y: roofLine.y1 })
|
||||
target = { x: roofLine.x1 + xSignStart * dist, y: roofLine.y1 }
|
||||
}
|
||||
if (!__isNearSkVertex(target)) {
|
||||
console.log(`⏭️ [pair#${index}] ${condition} HIP_SKIP_VERTEX_MISS start target=(${target.x.toFixed(1)},${target.y.toFixed(1)})`)
|
||||
} else {
|
||||
createHipLine(sPoint, target)
|
||||
}
|
||||
}
|
||||
|
||||
if (checkDist2 > 0) {
|
||||
let target
|
||||
if (moveDistY2 > moveDistX2) {
|
||||
const dist = moveDistY2 - moveDistX2
|
||||
createHipLine(ePoint, { x: roofLine.x2, y: roofLine.y2 + ySignEnd * dist })
|
||||
target = { x: roofLine.x2, y: roofLine.y2 + ySignEnd * dist }
|
||||
} else {
|
||||
const dist = moveDistX2 - moveDistY2
|
||||
createHipLine(ePoint, { x: roofLine.x2 + xSignEnd * dist, y: roofLine.y2 })
|
||||
target = { x: roofLine.x2 + xSignEnd * dist, y: roofLine.y2 }
|
||||
}
|
||||
if (!__isNearSkVertex(target)) {
|
||||
console.log(`⏭️ [pair#${index}] ${condition} HIP_SKIP_VERTEX_MISS end target=(${target.x.toFixed(1)},${target.y.toFixed(1)})`)
|
||||
} else {
|
||||
createHipLine(ePoint, target)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1794,6 +1816,21 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver
|
||||
}
|
||||
} else if (condition === 'left_out') {
|
||||
|
||||
// [SHOULDER-ADJACENT _out] 인접 pair 흡수 시 현재 pair 의 끝점은 새 SK 토폴로지에서 dissolve 됨.
|
||||
// roof.points 옛 코너 참조 → updateAndAddLine NOT FOUND + phantom eaveHelpLine 외부 출현.
|
||||
// processInBoth 의 동일 패턴 가드(line 1471-1481) 와 동일 임계.
|
||||
{
|
||||
const n = sortWallBaseLines.length
|
||||
const prevBL = sortWallBaseLines[(index - 1 + n) % n]
|
||||
const nextBL = sortWallBaseLines[(index + 1) % n]
|
||||
const prevAbsorbed = (prevBL?.attributes?.planeSize ?? Infinity) < 10
|
||||
const nextAbsorbed = (nextBL?.attributes?.planeSize ?? Infinity) < 10
|
||||
if (prevAbsorbed || nextAbsorbed) {
|
||||
console.log(`⏭️ [pair#${index}] ${condition} OUT_SKIP adj_absorbed prev=${prevAbsorbed} next=${nextAbsorbed}`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (isStartEnd.start) {
|
||||
const moveDist = Big(wallLine.x1).minus(wallBaseLine.x1).abs().toNumber()
|
||||
const aStartY = Big(roofLine.y1).minus(moveDist).toNumber()
|
||||
@ -1903,6 +1940,21 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver
|
||||
|
||||
} else if (condition === 'right_out') {
|
||||
|
||||
// [SHOULDER-ADJACENT _out] 인접 pair 흡수 시 현재 pair 의 끝점은 새 SK 토폴로지에서 dissolve 됨.
|
||||
// roof.points 옛 코너 참조 → updateAndAddLine NOT FOUND + phantom eaveHelpLine 외부 출현.
|
||||
// processInBoth 의 동일 패턴 가드(line 1471-1481) 와 동일 임계.
|
||||
{
|
||||
const n = sortWallBaseLines.length
|
||||
const prevBL = sortWallBaseLines[(index - 1 + n) % n]
|
||||
const nextBL = sortWallBaseLines[(index + 1) % n]
|
||||
const prevAbsorbed = (prevBL?.attributes?.planeSize ?? Infinity) < 10
|
||||
const nextAbsorbed = (nextBL?.attributes?.planeSize ?? Infinity) < 10
|
||||
if (prevAbsorbed || nextAbsorbed) {
|
||||
console.log(`⏭️ [pair#${index}] ${condition} OUT_SKIP adj_absorbed prev=${prevAbsorbed} next=${nextAbsorbed}`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (isStartEnd.start) {
|
||||
console.log('right_out::::isStartEnd:::::', isStartEnd)
|
||||
const moveDist = Big(wallLine.x1).minus(wallBaseLine.x1).abs().toNumber()
|
||||
@ -2049,6 +2101,21 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver
|
||||
}
|
||||
} else if (condition === 'top_out') {
|
||||
|
||||
// [SHOULDER-ADJACENT _out] 인접 pair 흡수 시 현재 pair 의 끝점은 새 SK 토폴로지에서 dissolve 됨.
|
||||
// roof.points 옛 코너 참조 → updateAndAddLine NOT FOUND + phantom eaveHelpLine 외부 출현.
|
||||
// processInBoth 의 동일 패턴 가드(line 1471-1481) 와 동일 임계.
|
||||
{
|
||||
const n = sortWallBaseLines.length
|
||||
const prevBL = sortWallBaseLines[(index - 1 + n) % n]
|
||||
const nextBL = sortWallBaseLines[(index + 1) % n]
|
||||
const prevAbsorbed = (prevBL?.attributes?.planeSize ?? Infinity) < 10
|
||||
const nextAbsorbed = (nextBL?.attributes?.planeSize ?? Infinity) < 10
|
||||
if (prevAbsorbed || nextAbsorbed) {
|
||||
console.log(`⏭️ [pair#${index}] ${condition} OUT_SKIP adj_absorbed prev=${prevAbsorbed} next=${nextAbsorbed}`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (isStartEnd.start) {
|
||||
console.log('top_out isStartEnd:::::::', isStartEnd)
|
||||
const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber()
|
||||
@ -2156,6 +2223,22 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver
|
||||
}
|
||||
} else if (condition === 'bottom_out') {
|
||||
console.log('bottom_out isStartEnd:::::::', isStartEnd)
|
||||
|
||||
// [SHOULDER-ADJACENT _out] 인접 pair 흡수 시 현재 pair 의 끝점은 새 SK 토폴로지에서 dissolve 됨.
|
||||
// roof.points 옛 코너 참조 → updateAndAddLine NOT FOUND + phantom eaveHelpLine 외부 출현.
|
||||
// processInBoth 의 동일 패턴 가드(line 1471-1481) 와 동일 임계.
|
||||
{
|
||||
const n = sortWallBaseLines.length
|
||||
const prevBL = sortWallBaseLines[(index - 1 + n) % n]
|
||||
const nextBL = sortWallBaseLines[(index + 1) % n]
|
||||
const prevAbsorbed = (prevBL?.attributes?.planeSize ?? Infinity) < 10
|
||||
const nextAbsorbed = (nextBL?.attributes?.planeSize ?? Infinity) < 10
|
||||
if (prevAbsorbed || nextAbsorbed) {
|
||||
console.log(`⏭️ [pair#${index}] ${condition} OUT_SKIP adj_absorbed prev=${prevAbsorbed} next=${nextAbsorbed}`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (isStartEnd.start) {
|
||||
console.log('isStartEnd:::::::', isStartEnd)
|
||||
const moveDist = Big(wallLine.y1).minus(wallBaseLine.y1).abs().toNumber()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user