라인오버하면 평행라인으로 변경
This commit is contained in:
parent
936d490803
commit
0b0a1786da
@ -6,7 +6,7 @@ import { calculateAngle, drawGableRoof, drawRoofByAttribute, drawShedRoof, toGeo
|
||||
import * as turf from '@turf/turf'
|
||||
import { LINE_TYPE, POLYGON_TYPE } from '@/common/common'
|
||||
import Big from 'big.js'
|
||||
import { drawSkeletonRidgeRoof, verifyMoveBoundary } from '@/util/skeleton-utils'
|
||||
import { drawSkeletonRidgeRoof, drawSkeletonRidgeRoofFromBaseLines, verifyMoveBoundary } from '@/util/skeleton-utils'
|
||||
|
||||
export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
type: 'QPolygon',
|
||||
@ -380,21 +380,23 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
/**
|
||||
* 보조선 그리기
|
||||
* @param settingModalFirstOptions
|
||||
* @param forceRedraw - true 면 verifyMoveBoundary 'crossed' 가드를 우회하고 강제로 재빌드.
|
||||
* (예: 오버 이동 상태에서 wall.baseLines/roof.lines 는 그대로 두고 SK 만 재생성하고 싶을 때)
|
||||
*/
|
||||
drawHelpLine(settingModalFirstOptions) {
|
||||
// [보호 가드] 이번 이동이 골짜기 경계를 넘어가는 과한 이동이면
|
||||
// 아예 초기화/재빌드 자체를 수행하지 않고 현재 상태 그대로 유지한다.
|
||||
// ※ 기존 drawHelpLine 로직은 건드리지 않고, 진입부에 가드 한 블록만 추가.
|
||||
// ※ 최초 그리기·offset 변경 등 moveUpDown=0 / moveFlowLine=0 케이스는
|
||||
// verifyMoveBoundary 가 'ok' 를 돌려주므로 영향 없음.
|
||||
drawHelpLine(settingModalFirstOptions, forceRedraw = false) {
|
||||
// [정책] 오버 이동(verifyMoveBoundary='crossed') 처리:
|
||||
// - roofLine 은 그대로, wall.baseLines 는 이미 오버된 좌표로 mutate 된 상태.
|
||||
// - 보조선 싹 지우고 '오버된 wall.baseLines 좌표 그대로' SK 재빌드.
|
||||
// - 일반 경로(drawSkeletonRidgeRoof) 는 45도 대각 확장/roof 경계 클램핑이 들어가
|
||||
// 오버 polygon 이 왜곡 → 용마루 지붕 케이스는 전용 함수(drawSkeletonRidgeRoofFromBaseLines)로 분기.
|
||||
// - forceRedraw 인자는 명시적 수동 호출 의도 표현 용도로만 유지.
|
||||
let __verdict = 'unknown'
|
||||
try {
|
||||
const __verdict = verifyMoveBoundary(this.id, this.canvas)
|
||||
__verdict = verifyMoveBoundary(this.id, this.canvas)
|
||||
if (__verdict === 'crossed') {
|
||||
console.warn('[drawHelpLine] 경계 넘음(crossed) → 초기화/재빌드 스킵 (기존 innerLines/SK 유지)')
|
||||
return
|
||||
console.warn(`[drawHelpLine] 경계 넘음(crossed) → 오버 전용 경로(drawSkeletonRidgeRoofFromBaseLines) 사용 (forceRedraw=${forceRedraw})`)
|
||||
}
|
||||
} catch (e) {
|
||||
// 판정기 자체 실패는 무시하고 기존 흐름 진행 (안전한 no-op)
|
||||
console.warn('[drawHelpLine] verifyMoveBoundary 예외 → 기존 흐름 진행:', e)
|
||||
}
|
||||
|
||||
@ -495,8 +497,12 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
|
||||
if (types.every((type) => type === LINE_TYPE.WALLLINE.EAVES)) {
|
||||
// 용마루 -- straight-skeleton
|
||||
// console.log('용마루 지붕')
|
||||
drawSkeletonRidgeRoof(this.id, this.canvas, textMode)
|
||||
// 오버(crossed) 이동 시 wall.baseLines 좌표 그대로 사용하는 전용 경로로 분기.
|
||||
if (__verdict === 'crossed') {
|
||||
drawSkeletonRidgeRoofFromBaseLines(this.id, this.canvas, textMode)
|
||||
} else {
|
||||
drawSkeletonRidgeRoof(this.id, this.canvas, textMode)
|
||||
}
|
||||
} else if (isGableRoof(types)) {
|
||||
// A형, B형 박공 지붕
|
||||
// console.log('패턴 지붕')
|
||||
@ -510,6 +516,19 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 오버 이동(crossed) 상태여도 wall.baseLines / roof.lines / outerLine / lengthText 는 그대로 두고
|
||||
* 보조선(innerLines: eaveHelpLine, HIP, extensionLine, SK ridge 등) 만 제거 후 SK 재빌드.
|
||||
*
|
||||
* drawHelpLine 의 진입부 verifyMoveBoundary 가드만 우회하는 wrapper. 이후 흐름은 동일.
|
||||
* - 초기화 필터(line 405-411) 가 이미 wall/roof/baseLine/outerLine/lengthText 를 제외하고 있어
|
||||
* 해당 객체들은 보존됨.
|
||||
* - 사용자가 오버된 상태에서 "SK 만 다시 그려보기" 의도로 명시적으로 호출할 때 사용.
|
||||
*/
|
||||
redrawHelpLineForced(settingModalFirstOptions) {
|
||||
return this.drawHelpLine(settingModalFirstOptions, true)
|
||||
},
|
||||
|
||||
addLengthText() {
|
||||
if ([POLYGON_TYPE.MODULE, 'arrow', POLYGON_TYPE.MODULE_SETUP_SURFACE, POLYGON_TYPE.OBJECT_SURFACE].includes(this.name)) {
|
||||
return
|
||||
|
||||
@ -11,6 +11,14 @@ import { calcLinePlaneSize } from '@/util/qpolygon-utils'
|
||||
import { getSelectLinePosition } from '@/util/skeleton-utils'
|
||||
import { useMouse } from '@/hooks/useMouse'
|
||||
|
||||
// [정책 flag] 오버 이동(인접 라인 반전 = polygon self-cross) 입력 처리 방법.
|
||||
// 'clamp' : (기본/2026-04-24 최종) 평행 위치-OVER_EPS 까지 클램핑. wall.baseLines 는 항상 정상 polygon, SK 확장 정상.
|
||||
// 'reject' : (2026-04-22 정책) silent skip — 오버면 해당 target 의 mutation 자체를 건너뜀. 토스트 없음.
|
||||
// 'passthrough' : 가드 전면 스킵 — 사용자가 입력한 raw 값을 그대로 적용. (과거 crossed 허용 동작; polygon 왜곡 위험)
|
||||
// 교체 방법: 이 상수만 변경. OVER_GUARD 분기가 자동 반응.
|
||||
const OVER_MOVE_POLICY = 'clamp'
|
||||
const OVER_EPS = 0.5
|
||||
|
||||
//동선이동 형 올림 내림
|
||||
export function useMovementSetting(id) {
|
||||
const TYPE = {
|
||||
@ -746,6 +754,57 @@ export function useMovementSetting(id) {
|
||||
deltaX = value.toNumber()
|
||||
}
|
||||
|
||||
// [OVER_GUARD] 오버 이동(인접 라인 반전) 처리. 정책은 파일 상단 OVER_MOVE_POLICY 로 전환.
|
||||
// 한계 계산 (공통):
|
||||
// nLimit = (nextLine 의 free 끝점 좌표) - (currentLine 의 해당 끝점 좌표)
|
||||
// pLimit = (prevLine 의 free 끝점 좌표) - (currentLine 의 해당 시작점 좌표)
|
||||
// delta 와 같은 부호인 한계만 후보(반대 부호 한계는 인접 라인이 길어지는 방향).
|
||||
// 정책별 동작:
|
||||
// 'passthrough' → 가드 전체 스킵
|
||||
// 'reject' → |delta| > |한계-EPS| 이면 해당 target 은 mutation 없이 건너뜀 (silent skip)
|
||||
// 'clamp' → |delta| 를 |한계-EPS| 로 잘라 평행 직전까지 이동. roof.moveUpDown 도 동일 비율 동기화.
|
||||
if (OVER_MOVE_POLICY !== 'passthrough') {
|
||||
const __isHoriz = currentLine.y1 === currentLine.y2
|
||||
const __raw = __isHoriz ? deltaY : deltaX
|
||||
const __sgn = Math.sign(__raw)
|
||||
if (__sgn !== 0) {
|
||||
const __nLimit = __isHoriz ? (nextLine.y2 - currentLine.y2) : (nextLine.x2 - currentLine.x2)
|
||||
const __pLimit = __isHoriz ? (prevLine.y1 - currentLine.y1) : (prevLine.x1 - currentLine.x1)
|
||||
let __absLimit = Infinity
|
||||
if (Math.sign(__nLimit) === __sgn) __absLimit = Math.min(__absLimit, Math.abs(__nLimit))
|
||||
if (Math.sign(__pLimit) === __sgn) __absLimit = Math.min(__absLimit, Math.abs(__pLimit))
|
||||
// [EPS] 평행 정확히 도달 시 인접 baseLine zero-length → polygon 중복 꼭짓점 → SkeletonBuilder 헛선 발생.
|
||||
// OVER_EPS 여유 두어 인접 baseLine 길이 OVER_EPS(=0.5) 로 남기고 SHOULDER_ABSORBED(planeSize<1) 가 자연 흡수.
|
||||
const __safeAbsLimit = Math.max(0, __absLimit - OVER_EPS)
|
||||
const __isOver = Math.abs(__raw) > __safeAbsLimit + 0.001
|
||||
|
||||
if (__isOver && OVER_MOVE_POLICY === 'reject') {
|
||||
// silent skip: mutation 자체 건너뜀. movingLineFromSkeleton 의 roof.points 확장도 막으려면 moveUpDown=0.
|
||||
console.warn(
|
||||
`[handleSave] OVER 거부(reject): ${__isHoriz ? 'deltaY' : 'deltaX'}=${__raw.toFixed(1)} > limit=${__safeAbsLimit.toFixed(1)} → target skip`
|
||||
)
|
||||
roof.moveUpDown = 0
|
||||
return
|
||||
}
|
||||
|
||||
if (OVER_MOVE_POLICY === 'clamp') {
|
||||
const __limited = __sgn * Math.min(Math.abs(__raw), __safeAbsLimit)
|
||||
if (Math.abs(__limited - __raw) > 0.001) {
|
||||
console.warn(
|
||||
`[handleSave] OVER 클램핑: ${__isHoriz ? 'deltaY' : 'deltaX'} ${__raw.toFixed(1)} → ${__limited.toFixed(1)} (nLimit=${__nLimit.toFixed(1)} pLimit=${__pLimit.toFixed(1)}, EPS=${OVER_EPS})`
|
||||
)
|
||||
if (__isHoriz) deltaY = __limited
|
||||
else deltaX = __limited
|
||||
// [SYNC roof.moveUpDown] movingLineFromSkeleton 은 roof.moveUpDown(mm) 로 roof.points 확장.
|
||||
// delta 만 줄이고 moveUpDown 방치 시 wall 은 평행까지, roof.points 는 원본 양만큼 이동 →
|
||||
// polygon 자기교차 → removeNonOrthogonalPoints 가 꼭짓점 축소 → "SK 가 wall 안에서 멈춤".
|
||||
// __limited(cm) × 10 = mm.
|
||||
roof.moveUpDown = Math.round(Math.abs(__limited) * 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentLine.set({
|
||||
x1: currentLine.x1 + deltaX,
|
||||
y1: currentLine.y1 + deltaY,
|
||||
|
||||
@ -783,14 +783,17 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
|
||||
// 6. 마루이동(용마루 위치 수동 조정)이 설정된 경우 movingLineFromSkeleton 결과로 대체
|
||||
|
||||
if (moveFlowLine !== 0 || moveUpDown !== 0) {
|
||||
// [보호 가드] 이동이 골짜기 경계를 넘어가는지 사전 판정
|
||||
// 'crossed' → 외곽선을 뒤집을 만큼 과한 이동 → 기존 SK/innerLines/skeletonLines 전부 보존 후 조용히 종료
|
||||
// 'ok' | 'on-boundary' | 'unknown' → 기존 흐름 그대로 진행
|
||||
// ※ 기존 로직은 전혀 수정하지 않고, 진입부에 한 블록만 추가
|
||||
const __moveVerdict = verifyMoveBoundary(roofId, canvas)
|
||||
if (__moveVerdict === 'crossed') {
|
||||
console.warn('[skeleton] 경계 넘음(crossed) → 재빌드 스킵 (기존 SK/innerLines/skeletonLines 유지)')
|
||||
return
|
||||
// [정책] 오버 이동(verifyMoveBoundary='crossed') 처리:
|
||||
// - 과거: 'crossed' 시 재빌드 스킵하여 기존 SK 유지 (silent skip).
|
||||
// - 현재: 사용자가 재작도 회피용 단축 수단으로 오버를 의도 사용 → 오버된 wall.baseLines 좌표 기반 재빌드 수행.
|
||||
// - 경고 로그만 남기고 기존 흐름 그대로 진행.
|
||||
try {
|
||||
const __moveVerdict = verifyMoveBoundary(roofId, canvas)
|
||||
if (__moveVerdict === 'crossed') {
|
||||
console.warn('[skeleton] 경계 넘음(crossed) → 오버된 wall.baseLine 좌표 기반 재빌드 진행')
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[skeleton] verifyMoveBoundary 예외 → 기존 흐름 진행:', e)
|
||||
}
|
||||
|
||||
const movedPoints = safeMovedPointsWithFallback(roofId, canvas)
|
||||
@ -901,18 +904,16 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
// [분기] 스켈레톤 입력 포인트 결정
|
||||
// 정상: changRoofLinePoints 그대로 사용 (기존 로직 변경 없음)
|
||||
// 오버: calcOverCorrectedPoints() 별도 함수로 보정 포인트 생성
|
||||
// ※ changRoofLinePoints 자체는 절대 수정하지 않음
|
||||
// ※ 오버 보정 실패 시 changRoofLinePoints 그대로 fallback
|
||||
// 정상: changRoofLinePoints 그대로 사용
|
||||
// 오버(일반 경로로 들어온 에지 케이스): calcOverCorrectedPoints() 로 보정 포인트 생성
|
||||
// ※ verifyMoveBoundary === 'crossed' 케이스는 drawHelpLine 에서 drawSkeletonRidgeRoofFromBaseLines
|
||||
// 로 사전 분기하므로, 여기 isOverDetected 분기는 일반 경로의 안전망 용도로만 작동.
|
||||
// ※ changRoofLinePoints 자체는 절대 수정하지 않음. 실패 시 그대로 fallback.
|
||||
// ──────────────────────────────────────────────────────────────────
|
||||
let skeletonInputPoints = changRoofLinePoints // 정상 경로
|
||||
let skeletonInputPoints = changRoofLinePoints
|
||||
|
||||
if (isOverDetected) {
|
||||
// [예외] 오버 감지 시 → 별도 함수에서 스켈레톤 전용 보정 포인트 계산
|
||||
try {
|
||||
// 누적 이동 보호: 이전 SK 확정 상태(lastPoints)를 전달하여
|
||||
// 1차 이동에서 이미 확정된 점은 보정 대상에서 제외한다.
|
||||
const corrected = calcOverCorrectedPointsSafe(
|
||||
changRoofLinePoints,
|
||||
origPoints,
|
||||
@ -923,7 +924,6 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -1006,6 +1006,102 @@ export const skeletonBuilder = (roofId, canvas, textMode) => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 오버 이동(verifyMoveBoundary='crossed') 전용 경로.
|
||||
*
|
||||
* 일반 경로(drawSkeletonRidgeRoof) 는 wall.baseLines → 45도 대각 확장 → roof 경계까지 늘린 polygon 을
|
||||
* SkeletonBuilder 에 입력한다. 오버 이동 상태에선 이 확장/클램핑이 폴리곤 자가교차나 roof 경계로의
|
||||
* 되돌림을 야기해 엉뚱한 SK 를 만든다.
|
||||
*
|
||||
* 본 함수는 **wall.baseLines 의 끝점을 확장/offset 없이 그대로** polygon 으로 넘겨 SK 를 빌드한다.
|
||||
* - roof.points / roof.lines / outerLine / lengthText 는 건드리지 않음.
|
||||
* - SHOULDER_ABSORBED (planeSize < 1) baseLines 는 스킵.
|
||||
* - innerLines 는 기존 createInnerLinesFromSkeleton 으로 생성 (isOverDetected=true 마킹).
|
||||
* - canvas.skeleton.lastPoints 는 기존 값을 보존 (오버 상태에서 새로 계산하지 않음).
|
||||
*/
|
||||
export const drawSkeletonRidgeRoofFromBaseLines = (roofId, canvas, textMode) => {
|
||||
const roof = canvas?.getObjects().find((o) => o.id === roofId)
|
||||
if (!roof) {
|
||||
console.warn('[SK_OVER_FN] roof 없음 → 중단')
|
||||
return
|
||||
}
|
||||
const wall = canvas.getObjects().find((obj) => obj.name === POLYGON_TYPE.WALL && obj.attributes?.roofId === roofId)
|
||||
if (!wall || !wall.baseLines?.length) {
|
||||
console.warn('[SK_OVER_FN] wall/baseLines 없음 → 중단')
|
||||
return
|
||||
}
|
||||
|
||||
// wall.baseLines 순차 끝점(x1,y1) 수집. zero-length(SHOULDER_ABSORBED) 항목 스킵.
|
||||
const rawPoints = []
|
||||
for (let i = 0; i < wall.baseLines.length; i++) {
|
||||
const bl = wall.baseLines[i]
|
||||
const planeSize = bl?.attributes?.planeSize ?? Infinity
|
||||
if (planeSize < 1) {
|
||||
console.log(`[SK_OVER_FN] baseLines[${i}] SHOULDER_ABSORBED skip (planeSize=${planeSize})`)
|
||||
continue
|
||||
}
|
||||
if (bl == null || !isFinite(bl.x1) || !isFinite(bl.y1)) {
|
||||
console.warn(`[SK_OVER_FN] baseLines[${i}] invalid coord → skip`)
|
||||
continue
|
||||
}
|
||||
rawPoints.push({ x: bl.x1, y: bl.y1 })
|
||||
}
|
||||
|
||||
if (rawPoints.length < 3) {
|
||||
console.warn(`[SK_OVER_FN] 유효 baseLines 끝점 ${rawPoints.length} < 3 → 중단`)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[SK_OVER_FN] wall.baseLines polygon:', rawPoints.map((p, i) => `[${i}](${Math.round(p.x)},${Math.round(p.y)})`))
|
||||
|
||||
// skeletonPoints 마킹 (오버 좌표 그대로)
|
||||
roof.skeletonPoints = rawPoints.map((p) => ({ x: p.x, y: p.y }))
|
||||
|
||||
const perturbed = perturbPolygonPoints(rawPoints)
|
||||
const geoJSONPolygon = toGeoJSON(perturbed)
|
||||
|
||||
try {
|
||||
geoJSONPolygon.pop()
|
||||
console.log('[SK_OVER_FN] geoJSONPolygon:', JSON.stringify(geoJSONPolygon, null, 2))
|
||||
console.log('[SK_OVER_FN] 꼭짓점 수:', geoJSONPolygon.length)
|
||||
const skeleton = SkeletonBuilder.BuildFromGeoJSON([[geoJSONPolygon]])
|
||||
|
||||
roof.innerLines = roof.innerLines || []
|
||||
roof.innerLines = createInnerLinesFromSkeleton(roofId, canvas, skeleton, textMode, /* isOverDetected */ true)
|
||||
|
||||
if (!canvas.skeletonStates) {
|
||||
canvas.skeletonStates = {}
|
||||
canvas.skeletonLines = []
|
||||
}
|
||||
canvas.skeletonStates[roofId] = true
|
||||
canvas.skeletonLines = []
|
||||
canvas.skeletonLines.push(...roof.innerLines)
|
||||
roof.skeletonLines = canvas.skeletonLines
|
||||
|
||||
const cleanSkeleton = {
|
||||
Edges: skeleton.Edges.map((edge) => ({
|
||||
X1: edge.Edge.Begin.X,
|
||||
Y1: edge.Edge.Begin.Y,
|
||||
X2: edge.Edge.End.X,
|
||||
Y2: edge.Edge.End.Y,
|
||||
Polygon: edge.Polygon,
|
||||
})),
|
||||
roofId: roofId,
|
||||
}
|
||||
// lastPoints 는 기존 값 유지 (오버 상태에서 새 누적 기준점을 만들지 않음)
|
||||
const __preservedLastPoints = canvas.skeleton?.lastPoints ?? null
|
||||
canvas.skeleton = cleanSkeleton
|
||||
if (__preservedLastPoints) canvas.skeleton.lastPoints = __preservedLastPoints
|
||||
|
||||
canvas.set('skeleton', cleanSkeleton)
|
||||
canvas.renderAll()
|
||||
} catch (e) {
|
||||
console.error('[SK_OVER_FN] 스켈레톤 생성 오류:', e)
|
||||
console.error('[SK_OVER_FN] 입력 좌표:', rawPoints.map((p, i) => `[${i}] (${p.x.toFixed(2)},${p.y.toFixed(2)})`))
|
||||
if (canvas.skeletonStates) canvas.skeletonStates[roofId] = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 스켈레톤 결과와 외벽선 정보를 바탕으로 내부선(용마루, 추녀)을 생성합니다.
|
||||
* @param {object} skeleton - SkeletonBuilder로부터 반환된 스켈레톤 객체
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user