Merge branch 'dev' of https://git.hanasys.jp/qcast3/qcast-front into feature/redo-undo
# Conflicts: # src/hooks/surface/usePlacementShapeDrawing.js
This commit is contained in:
commit
ca8778ba31
@ -6,7 +6,89 @@ 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'
|
||||
|
||||
// ========================================================================
|
||||
// [DEBUG] 캔버스 라인 라벨 — 로컬 전용 (NEXT_PUBLIC_RUN_MODE === 'local')
|
||||
// ----------------------------------------------------------------------
|
||||
// 별도 util 파일을 두지 않고 QPolygon.js 안에 인라인.
|
||||
// dev/prd 에선 가드가 즉시 false → 코드 미실행. 외부 의존성 없음.
|
||||
// 라인 자체의 stroke/fill 은 절대 손대지 않고 fabric.Text 만 추가.
|
||||
// ========================================================================
|
||||
const DEBUG_LABEL_NAME = '__debugLabel'
|
||||
|
||||
function __isDebugLabelsEnabled() {
|
||||
// 디버그 라벨(H-1, B-4, RG-1 등) 표시 비활성화
|
||||
return false
|
||||
}
|
||||
|
||||
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',
|
||||
@ -385,21 +467,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)
|
||||
}
|
||||
|
||||
@ -500,8 +584,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('패턴 지붕')
|
||||
@ -513,6 +601,68 @@ export const QPolygon = fabric.util.createClass(fabric.Polygon, {
|
||||
// console.log('변별로 설정')
|
||||
drawRoofByAttribute(this.id, this.canvas, textMode)
|
||||
}
|
||||
|
||||
// 대칭 hip line 쌍의 planeSize/actualSize 를 평균값으로 통일 (예: 1637 / 1638 → 1637.5)
|
||||
// hip 라인은 좌우 대칭이지만 부동소수점 round-off 로 1mm 차이가 발생함
|
||||
if (this.innerLines && this.innerLines.length > 0) {
|
||||
const hips = this.innerLines.filter((l) => l && l.name === LINE_TYPE.SUBLINE.HIP)
|
||||
const _segLen = (l) => Math.round(Math.sqrt((l.x2 - l.x1) ** 2 + (l.y2 - l.y1) ** 2) * 10)
|
||||
const _equalizeText = (line, plane, actual) => {
|
||||
if (line.attributes) {
|
||||
line.attributes.planeSize = plane
|
||||
line.attributes.actualSize = actual
|
||||
}
|
||||
// canvas 위 lengthText 도 즉시 갱신
|
||||
const text = this.canvas.getObjects().find((o) => o.name === 'lengthText' && o.parentId === line.id)
|
||||
if (text) {
|
||||
text.set({ planeSize: plane, actualSize: actual })
|
||||
// 현재 표시 모드에 맞춰 text 갱신
|
||||
const isPlane = !text.actualSize || text.text === String(text.planeSize)
|
||||
text.set({ text: isPlane ? String(plane) : String(actual) })
|
||||
}
|
||||
}
|
||||
// 길이 그룹화: ±5mm 이내 길이끼리 묶고, 그 그룹 안에서 평균값으로 통일
|
||||
const used = new Set()
|
||||
hips.forEach((h, i) => {
|
||||
if (used.has(i)) return
|
||||
const group = [{ idx: i, line: h }]
|
||||
const baseLen = _segLen(h)
|
||||
hips.forEach((h2, j) => {
|
||||
if (j <= i || used.has(j)) return
|
||||
if (Math.abs(_segLen(h2) - baseLen) <= 5) group.push({ idx: j, line: h2 })
|
||||
})
|
||||
if (group.length >= 2) {
|
||||
const avgPlane = group.reduce((s, g) => s + (g.line.attributes?.planeSize || 0), 0) / group.length
|
||||
const avgActual = group.reduce((s, g) => s + (g.line.attributes?.actualSize || 0), 0) / group.length
|
||||
// 0.5 단위 정밀도 유지
|
||||
const plane = Math.round(avgPlane * 2) / 2
|
||||
const actual = Math.round(avgActual * 2) / 2
|
||||
group.forEach(({ idx, line }) => {
|
||||
used.add(idx)
|
||||
_equalizeText(line, plane, actual)
|
||||
})
|
||||
} else {
|
||||
used.add(i)
|
||||
}
|
||||
})
|
||||
this.canvas?.renderAll()
|
||||
}
|
||||
|
||||
// [DEBUG] 로컬(NEXT_PUBLIC_RUN_MODE==='local') 에서만 라벨 부착. 그 외 즉시 return.
|
||||
__attachDebugLabels(this.canvas, this.id)
|
||||
},
|
||||
|
||||
/**
|
||||
* 오버 이동(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() {
|
||||
|
||||
@ -10,6 +10,7 @@ import Angle from '@/components/floor-plan/modal/lineTypes/Angle'
|
||||
import DoublePitch from '@/components/floor-plan/modal/lineTypes/DoublePitch'
|
||||
import Diagonal from '@/components/floor-plan/modal/lineTypes/Diagonal'
|
||||
import { usePopup } from '@/hooks/usePopup'
|
||||
import { useSwal } from '@/hooks/useSwal'
|
||||
import { useState } from 'react'
|
||||
import Image from 'next/image'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
@ -18,6 +19,7 @@ export default function WallLineSetting(props) {
|
||||
const { id } = props
|
||||
const { addPopup, closePopup } = usePopup()
|
||||
const { getMessage } = useMessage()
|
||||
const { swalFire } = useSwal()
|
||||
const [propertiesId, setPropertiesId] = useState(uuidv4())
|
||||
const [useCalcPad, setUseCalcPad] = useState(false)
|
||||
const {
|
||||
@ -182,10 +184,11 @@ export default function WallLineSetting(props) {
|
||||
<button
|
||||
className="btn-frame modal act"
|
||||
onClick={() => {
|
||||
handleFix()
|
||||
// closePopup(id)
|
||||
|
||||
// setShowPropertiesSettingModal(true)
|
||||
swalFire({
|
||||
type: 'confirm',
|
||||
text: getMessage('modal.cover.outline.fix.confirm'),
|
||||
confirmFn: handleFix,
|
||||
})
|
||||
}}
|
||||
>
|
||||
{getMessage('modal.cover.outline.fix')}
|
||||
|
||||
@ -9,11 +9,13 @@ import { OUTER_LINE_TYPE } from '@/store/outerLineAtom'
|
||||
import OuterLineWall from '@/components/floor-plan/modal/lineTypes/OuterLineWall'
|
||||
import { usePlacementShapeDrawing } from '@/hooks/surface/usePlacementShapeDrawing'
|
||||
import { usePopup } from '@/hooks/usePopup'
|
||||
import { useSwal } from '@/hooks/useSwal'
|
||||
import Image from 'next/image'
|
||||
|
||||
export default function PlacementShapeDrawing({ id, pos = { x: 50, y: 230 } }) {
|
||||
const { getMessage } = useMessage()
|
||||
const { closePopup } = usePopup()
|
||||
const { swalFire } = useSwal()
|
||||
const [buttonAct, setButtonAct] = useState(1)
|
||||
const [useCalcPad, setUseCalcPad] = useState(false)
|
||||
const types = [
|
||||
@ -155,7 +157,16 @@ export default function PlacementShapeDrawing({ id, pos = { x: 50, y: 230 } }) {
|
||||
<button className="btn-frame modal mr5" onClick={handleRollback}>
|
||||
{getMessage('modal.cover.outline.rollback')}
|
||||
</button>
|
||||
<button className="btn-frame modal act" onClick={handleFix}>
|
||||
<button
|
||||
className="btn-frame modal act"
|
||||
onClick={() => {
|
||||
swalFire({
|
||||
type: 'confirm',
|
||||
text: getMessage('modal.cover.outline.fix.confirm'),
|
||||
confirmFn: handleFix,
|
||||
})
|
||||
}}
|
||||
>
|
||||
{getMessage('modal.placement.surface.drawing.fix')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -12,6 +12,14 @@ import { getSelectLinePosition } from '@/util/skeleton-utils'
|
||||
import { useMouse } from '@/hooks/useMouse'
|
||||
import { useUndoRedo } from '@/hooks/useUndoRedo'
|
||||
|
||||
// [정책 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 = {
|
||||
@ -749,6 +757,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,
|
||||
|
||||
@ -32,6 +32,8 @@ import { calculateAngle } from '@/util/qpolygon-utils'
|
||||
import { fabric } from 'fabric'
|
||||
import { outlineDisplaySelector } from '@/store/settingAtom'
|
||||
import { usePopup } from '@/hooks/usePopup'
|
||||
import { useSwal } from '@/hooks/useSwal'
|
||||
import { useMessage } from '@/hooks/useMessage'
|
||||
import Big from 'big.js'
|
||||
import RoofShapeSetting from '@/components/floor-plan/modal/roofShape/RoofShapeSetting'
|
||||
import { useObject } from '@/hooks/useObject'
|
||||
@ -88,6 +90,8 @@ export function useOuterLineWall(id, propertiesId) {
|
||||
const arrow1Ref = useRef(arrow1)
|
||||
const arrow2Ref = useRef(arrow2)
|
||||
const { addPopup, closePopup } = usePopup()
|
||||
const { swalFire } = useSwal()
|
||||
const { getMessage } = useMessage()
|
||||
|
||||
const setOuterLineFix = useSetRecoilState(outerLineFixState)
|
||||
const setCurrentMenu = useSetRecoilState(currentMenuState)
|
||||
@ -937,7 +941,11 @@ export function useOuterLineWall(id, propertiesId) {
|
||||
|
||||
const enterCheck = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleFix()
|
||||
swalFire({
|
||||
type: 'confirm',
|
||||
text: getMessage('modal.cover.outline.fix.confirm'),
|
||||
confirmFn: handleFix,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -22,6 +22,8 @@ import {
|
||||
import { usePolygon } from '@/hooks/usePolygon'
|
||||
import { POLYGON_TYPE } from '@/common/common'
|
||||
import { usePopup } from '@/hooks/usePopup'
|
||||
import { useSwal } from '@/hooks/useSwal'
|
||||
import { useMessage } from '@/hooks/useMessage'
|
||||
import { useSurfaceShapeBatch } from './useSurfaceShapeBatch'
|
||||
|
||||
import { roofDisplaySelector } from '@/store/settingAtom'
|
||||
@ -50,6 +52,8 @@ export function usePlacementShapeDrawing(id) {
|
||||
const { changeSurfaceLineType } = useSurfaceShapeBatch({})
|
||||
const { handleSelectableObjects } = useObject()
|
||||
const { saveSnapshot, popLastSnapshot } = useUndoRedo()
|
||||
const { swalFire } = useSwal()
|
||||
const { getMessage } = useMessage()
|
||||
|
||||
const verticalHorizontalMode = useRecoilValue(verticalHorizontalModeState)
|
||||
const adsorptionRange = useRecoilValue(adsorptionRangeState)
|
||||
@ -1097,7 +1101,11 @@ export function usePlacementShapeDrawing(id) {
|
||||
|
||||
const enterCheck = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleFix()
|
||||
swalFire({
|
||||
type: 'confirm',
|
||||
text: getMessage('modal.cover.outline.fix.confirm'),
|
||||
confirmFn: handleFix,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -215,7 +215,8 @@ export const useLine = () => {
|
||||
|
||||
line.attributes = {
|
||||
...line.attributes,
|
||||
actualSize: Number(line.attributes.actualSize.toFixed(0)),
|
||||
// 0.5 단위 정밀도 유지 (대칭 분할 시 1637.5 같은 값 보존). 그 외에는 정수.
|
||||
actualSize: Number((Math.round(line.attributes.actualSize * 2) / 2).toFixed(1)),
|
||||
isCalculated: true,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1777,7 +1777,8 @@ export function useMode() {
|
||||
const offsetEdges = []
|
||||
|
||||
polygon.edges.forEach((edge, i) => {
|
||||
const offset = lines[i % lines.length].attributes.offset === 0 ? 1 : lines[i % lines.length].attributes.offset
|
||||
// offset 이 0(처마/케라바 출폭 0) 인 경우 그대로 0 유지 → wall == roof
|
||||
const offset = lines[i % lines.length].attributes.offset
|
||||
const dx = edge.outwardNormal.x * offset
|
||||
const dy = edge.outwardNormal.y * offset
|
||||
offsetEdges.push(createOffsetEdge(edge, dx, dy))
|
||||
@ -1830,7 +1831,8 @@ export function useMode() {
|
||||
const offsetEdges = []
|
||||
|
||||
polygon.edges.forEach((edge, i) => {
|
||||
const offset = lines[i % lines.length].attributes.offset === 0 ? 1 : lines[i % lines.length].attributes.offset
|
||||
// offset 이 0(처마/케라바 출폭 0) 인 경우 그대로 0 유지 → wall == roof
|
||||
const offset = lines[i % lines.length].attributes.offset
|
||||
const dx = edge.inwardNormal.x * offset
|
||||
const dy = edge.inwardNormal.y * offset
|
||||
offsetEdges.push(createOffsetEdge(edge, dx, dy))
|
||||
@ -1977,10 +1979,20 @@ export function useMode() {
|
||||
const y1 = Big(line.y1)
|
||||
const y2 = Big(line.y2)
|
||||
const lineLength = x1.minus(x2).abs().pow(2).plus(y1.minus(y2).abs().pow(2)).sqrt().times(10).round().toNumber()
|
||||
const isDivLine = divWallLines.some((divLine) => divLine.index === index)
|
||||
|
||||
// 옵션 3: 외벽선의 입력 치수(attributes.planeSize/actualSize)를 오프셋 폴리곤 라인에도 propagate
|
||||
// → polygon-offset 알고리즘의 부동소수점 누적 오차로 인한 5~10mm drift 제거
|
||||
// divLine(외벽 분기로 인해 새로 삽입된 코너 라인)은 wall.lines 와 1:1 매칭이 아니므로
|
||||
// geometric lineLength 그대로 사용
|
||||
const sourceWallLine = !isDivLine ? wall.lines[roofWallIndex] : null
|
||||
const propagatedPlaneSize = sourceWallLine?.attributes?.planeSize ?? lineLength
|
||||
const propagatedActualSize = sourceWallLine?.attributes?.actualSize ?? lineLength
|
||||
|
||||
line.attributes = {
|
||||
roofId: roof.id,
|
||||
planeSize: lineLength,
|
||||
actualSize: lineLength,
|
||||
planeSize: propagatedPlaneSize,
|
||||
actualSize: propagatedActualSize,
|
||||
wallLine: wall.lines[roofWallIndex].id,
|
||||
type: wall.lines[roofWallIndex].attributes.type,
|
||||
offset: wall.lines[roofWallIndex].attributes.offset,
|
||||
@ -1989,7 +2001,6 @@ export function useMode() {
|
||||
sleeve: wall.lines[roofWallIndex].attributes.sleeve || false,
|
||||
}
|
||||
|
||||
const isDivLine = divWallLines.some((divLine) => divLine.index === index)
|
||||
if (!isDivLine) {
|
||||
roofWallIndex++
|
||||
}
|
||||
|
||||
@ -1053,26 +1053,53 @@ export const usePolygon = () => {
|
||||
const length1 = Math.round(Math.hypot(newLine1.x1 - newLine1.x2, newLine1.y1 - newLine1.y2)) * 10
|
||||
const length2 = Math.round(Math.hypot(newLine2.x1 - newLine2.x2, newLine2.y1 - newLine2.y2)) * 10
|
||||
|
||||
// 원본에 planeSize/actualSize가 있으면 비율로 분배, 없으면 기하학적 길이 사용
|
||||
let planeSize1, planeSize2, actualSize1, actualSize2
|
||||
if (line.attributes.planeSize && originalGeomLength > 0) {
|
||||
const ratio1 = length1 / originalGeomLength
|
||||
const ratio2 = length2 / originalGeomLength
|
||||
planeSize1 = Math.round(line.attributes.planeSize * ratio1)
|
||||
planeSize2 = Math.round(line.attributes.planeSize * ratio2)
|
||||
} else {
|
||||
planeSize1 = length1
|
||||
planeSize2 = length2
|
||||
// 분할 시 새 sub-segment 의 planeSize/actualSize 는 새 기하학으로 직접 계산.
|
||||
// 부모 비율을 쓰면 부모 좌표 drift 가 그대로 전파되어 사용자 기대 round 값과 어긋남.
|
||||
// sum 이 부모 입력 planeSize 와 ±20mm 차이일 때 잔차 보정:
|
||||
// - sum < parent (drift +): geomLen 이 underestimated → 잔차를 SHORTER 에 더해
|
||||
// LONGER 의 round 값(예: 910)을 보존
|
||||
// - sum > parent (drift -): geomLen 이 overestimated → 잔차를 LONGER 에서 빼서
|
||||
// SHORTER 의 round 값(예: 460)을 보존
|
||||
let planeSize1 = length1
|
||||
let planeSize2 = length2
|
||||
let actualSize1 = length1
|
||||
let actualSize2 = length2
|
||||
|
||||
const _redistribute = (parentVal, p1, p2) => {
|
||||
if (!parentVal) return [p1, p2]
|
||||
const sum = p1 + p2
|
||||
const drift = parentVal - sum
|
||||
if (Math.abs(drift) > 20) return [p1, p2] // 큰 차이는 보정하지 않음
|
||||
if (drift === 0) return [p1, p2]
|
||||
// drift > 0: SHORTER 에 잔차 추가 → LONGER 보존
|
||||
// drift < 0: LONGER 에 잔차 제거 → SHORTER 보존
|
||||
if (drift > 0) {
|
||||
if (length1 <= length2) return [parentVal - p2, p2]
|
||||
return [p1, parentVal - p1]
|
||||
} else {
|
||||
if (length1 >= length2) return [parentVal - p2, p2]
|
||||
return [p1, parentVal - p1]
|
||||
}
|
||||
}
|
||||
if (line.attributes.actualSize && originalGeomLength > 0) {
|
||||
const ratio1 = length1 / originalGeomLength
|
||||
const ratio2 = length2 / originalGeomLength
|
||||
actualSize1 = Math.round(line.attributes.actualSize * ratio1)
|
||||
actualSize2 = Math.round(line.attributes.actualSize * ratio2)
|
||||
} else {
|
||||
actualSize1 = length1
|
||||
actualSize2 = length2
|
||||
|
||||
const parentPlane = line.attributes?.planeSize
|
||||
const parentActual = line.attributes?.actualSize
|
||||
;[planeSize1, planeSize2] = _redistribute(parentPlane, planeSize1, planeSize2)
|
||||
;[actualSize1, actualSize2] = _redistribute(parentActual, actualSize1, actualSize2)
|
||||
|
||||
// 2분할에서 양 끝이 거의 같을 때(차이 ≤ 5mm) 대칭성 강제: 두 값을 평균으로 통일
|
||||
// 예: ridge 가 wall 을 거의 정확히 양분 → 1637 / 1638 → 둘 다 1637.5
|
||||
// 합이 홀수이면 0.5 소수점을 유지하여 양쪽이 정확히 같은 값으로 표시되도록 함
|
||||
const _equalizeIfNearSymmetric = (p1, p2) => {
|
||||
if (!p1 || !p2) return [p1, p2]
|
||||
if (Math.abs(p1 - p2) > 5) return [p1, p2]
|
||||
const avg = (p1 + p2) / 2 // 0.5 소수점 허용 (예: 3275 / 2 = 1637.5)
|
||||
// 0.5 단위로 정밀도 유지 (Big number/이상한 부동소수점 방지)
|
||||
const halfPrecision = Math.round(avg * 2) / 2
|
||||
return [halfPrecision, halfPrecision]
|
||||
}
|
||||
;[planeSize1, planeSize2] = _equalizeIfNearSymmetric(planeSize1, planeSize2)
|
||||
;[actualSize1, actualSize2] = _equalizeIfNearSymmetric(actualSize1, actualSize2)
|
||||
|
||||
newLine1.attributes = {
|
||||
...line.attributes,
|
||||
@ -1159,6 +1186,23 @@ export const usePolygon = () => {
|
||||
newLine.length = lastCalcLength
|
||||
|
||||
newLines.push(newLine)
|
||||
|
||||
// 홀수 분할(3, 5, ...) 시 양 끝(첫·마지막) sub-segment 의 길이를 평균값으로 동일하게 보정
|
||||
// 패턴 A/B 등 대칭 입력 wall 이 hip lines 로 분할될 때 좌우 대칭성을 강제로 유지
|
||||
const newSubsForThisLine = newLines.slice(-(intersections.length + 1)) // 이 부모 line 이 만든 sub-segment 들
|
||||
const segCount = newSubsForThisLine.length
|
||||
if (segCount >= 3 && segCount % 2 === 1) {
|
||||
const first = newSubsForThisLine[0]
|
||||
const last = newSubsForThisLine[segCount - 1]
|
||||
const avgPlane = Math.round((first.attributes.planeSize + last.attributes.planeSize) / 2)
|
||||
const avgActual = Math.round((first.attributes.actualSize + last.attributes.actualSize) / 2)
|
||||
// 두 끝값의 차이가 작을 때만 평균화 (대칭성 가정 케이스)
|
||||
if (Math.abs(first.attributes.planeSize - last.attributes.planeSize) <= 20) {
|
||||
first.attributes = { ...first.attributes, planeSize: avgPlane, actualSize: avgActual }
|
||||
last.attributes = { ...last.attributes, planeSize: avgPlane, actualSize: avgActual }
|
||||
}
|
||||
}
|
||||
|
||||
divideLines.splice(i, 1) // 기존 line 제거
|
||||
}
|
||||
}
|
||||
@ -1638,18 +1682,60 @@ export const usePolygon = () => {
|
||||
return startFlag && endFlag
|
||||
})
|
||||
|
||||
// 1차 매칭: 양 끝점이 정확히 일치하는 부모 line 의 attributes 복사
|
||||
const matchedRoofLines = new Set()
|
||||
roofLines.forEach((line) => {
|
||||
//console.log("::::::::::",line);
|
||||
roof.lines.forEach((roofLine) => {
|
||||
if (
|
||||
(isSamePoint(line.startPoint, roofLine.startPoint) && isSamePoint(line.endPoint, roofLine.endPoint)) ||
|
||||
(isSamePoint(line.startPoint, roofLine.endPoint) && isSamePoint(line.endPoint, roofLine.startPoint))
|
||||
) {
|
||||
roofLine.attributes = { ...line.attributes }
|
||||
matchedRoofLines.add(roofLine)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 2차 매칭 (옵션 3 + α): 대각선이 외벽 중간을 분할한 sub-segment 처리
|
||||
const _isCollinearWithin = (parent, p) => {
|
||||
const ax = parent.startPoint.x, ay = parent.startPoint.y
|
||||
const bx = parent.endPoint.x, by = parent.endPoint.y
|
||||
const dx = bx - ax, dy = by - ay
|
||||
const segLenSq = dx * dx + dy * dy
|
||||
if (segLenSq < 1) return false
|
||||
const cross = (p.x - ax) * dy - (p.y - ay) * dx
|
||||
if (Math.abs(cross) / Math.sqrt(segLenSq) > 2) return false
|
||||
const t = ((p.x - ax) * dx + (p.y - ay) * dy) / segLenSq
|
||||
return t >= -0.001 && t <= 1.001
|
||||
}
|
||||
const _segLen = (s, e) => Math.sqrt((s.x - e.x) ** 2 + (s.y - e.y) ** 2)
|
||||
|
||||
const parentCandidates = [...polygonLines, ...polygon.innerLines]
|
||||
roof.lines.forEach((roofLine) => {
|
||||
if (matchedRoofLines.has(roofLine)) return
|
||||
const parent = parentCandidates.find(
|
||||
(p) => p?.startPoint && p?.endPoint && _isCollinearWithin(p, roofLine.startPoint) && _isCollinearWithin(p, roofLine.endPoint),
|
||||
)
|
||||
if (!parent) return
|
||||
const parentLen = _segLen(parent.startPoint, parent.endPoint)
|
||||
if (parentLen < 1) return
|
||||
const subLen = _segLen(roofLine.startPoint, roofLine.endPoint)
|
||||
const ratio = subLen / parentLen
|
||||
|
||||
const parentAttrs = parent.attributes || {}
|
||||
const parentPlane = Number(parentAttrs.planeSize)
|
||||
const parentActual = Number(parentAttrs.actualSize)
|
||||
const propagatedPlane = Number.isFinite(parentPlane) ? Math.round(parentPlane * ratio) : null
|
||||
const propagatedActual = Number.isFinite(parentActual) ? Math.round(parentActual * ratio) : null
|
||||
|
||||
roofLine.attributes = {
|
||||
...parentAttrs,
|
||||
...(propagatedPlane != null ? { planeSize: propagatedPlane } : {}),
|
||||
...(propagatedActual != null ? { actualSize: propagatedActual } : {}),
|
||||
}
|
||||
matchedRoofLines.add(roofLine)
|
||||
})
|
||||
|
||||
// canvas.add(roof)
|
||||
createdRoofs.push(roof)
|
||||
canvas.remove(polygon)
|
||||
|
||||
@ -70,6 +70,7 @@
|
||||
"modal.cover.outline.length": "長さ(mm)",
|
||||
"modal.cover.outline.arrow": "方向(矢印)",
|
||||
"modal.cover.outline.fix": "外壁線確定",
|
||||
"modal.cover.outline.fix.confirm": "本当に確定しますか?確定後は修正できません。",
|
||||
"modal.cover.outline.rollback": "前に戻る",
|
||||
"modal.cover.outline.finish": "設定完了",
|
||||
"common.setting.finish": "設定完了",
|
||||
|
||||
@ -70,6 +70,7 @@
|
||||
"modal.cover.outline.length": "길이(mm)",
|
||||
"modal.cover.outline.arrow": "방향(화살표)",
|
||||
"modal.cover.outline.fix": "외벽선 확정",
|
||||
"modal.cover.outline.fix.confirm": "정말로 확정하시겠습니까? 확정 후에는 수정할 수 없습니다.",
|
||||
"modal.cover.outline.rollback": "일변전으로 돌아가기",
|
||||
"modal.cover.outline.finish": "설정완료",
|
||||
"common.setting.finish": "설정완료",
|
||||
|
||||
@ -726,8 +726,11 @@ export const drawGableRoof = (roofId, canvas, textMode) => {
|
||||
let currentRoof
|
||||
if (line.attributes.offset === 0) {
|
||||
checkRoofLines.forEach((roofLine) => {
|
||||
const isVerticalSame = line.y1 === roofLine.y1 && line.y2 === roofLine.y2
|
||||
const isHorizontalSame = line.x1 === roofLine.x1 && line.x2 === roofLine.x2
|
||||
// 정방향/역방향 양쪽 모두 매칭 허용 (좌표 정렬에 따라 끝점이 swap 될 수 있음)
|
||||
const isVerticalSame =
|
||||
(line.y1 === roofLine.y1 && line.y2 === roofLine.y2) || (line.y1 === roofLine.y2 && line.y2 === roofLine.y1)
|
||||
const isHorizontalSame =
|
||||
(line.x1 === roofLine.x1 && line.x2 === roofLine.x2) || (line.x1 === roofLine.x2 && line.x2 === roofLine.x1)
|
||||
if (
|
||||
(isVerticalSame || isHorizontalSame) &&
|
||||
(isPointOnLineNew(roofLine, { x: line.x1, y: line.y1 }) || isPointOnLineNew(roofLine, { x: line.x2, y: line.y2 }))
|
||||
@ -735,6 +738,11 @@ export const drawGableRoof = (roofId, canvas, textMode) => {
|
||||
currentRoof = { roofLine }
|
||||
}
|
||||
})
|
||||
// offset=0(처마/케라바 출폭 0) 케이스에서 매칭 실패하면 wall.line 자체를 roofLine 으로 사용
|
||||
// (wall == roof 이므로 자기 자신이 맞음). currentRoof 가 undefined 일 때 NPE 방지.
|
||||
if (!currentRoof) {
|
||||
currentRoof = { roofLine: line }
|
||||
}
|
||||
} else {
|
||||
const findEdge = { vertex1: { x: midX, y: midY }, vertex2: { x: midX + roofVector.x * offset, y: midY + roofVector.y * offset } }
|
||||
const edgeDx =
|
||||
@ -2041,8 +2049,11 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
|
||||
let currentRoof
|
||||
if (line.attributes.offset === 0) {
|
||||
checkRoofLines.forEach((roofLine) => {
|
||||
const isVerticalSame = line.y1 === roofLine.y1 && line.y2 === roofLine.y2
|
||||
const isHorizontalSame = line.x1 === roofLine.x1 && line.x2 === roofLine.x2
|
||||
// 정방향/역방향 양쪽 모두 매칭 허용 (좌표 정렬에 따라 끝점이 swap 될 수 있음)
|
||||
const isVerticalSame =
|
||||
(line.y1 === roofLine.y1 && line.y2 === roofLine.y2) || (line.y1 === roofLine.y2 && line.y2 === roofLine.y1)
|
||||
const isHorizontalSame =
|
||||
(line.x1 === roofLine.x1 && line.x2 === roofLine.x2) || (line.x1 === roofLine.x2 && line.x2 === roofLine.x1)
|
||||
if (
|
||||
(isVerticalSame || isHorizontalSame) &&
|
||||
(isPointOnLineNew(roofLine, { x: line.x1, y: line.y1 }) || isPointOnLineNew(roofLine, { x: line.x2, y: line.y2 }))
|
||||
@ -2050,6 +2061,11 @@ export const drawRoofByAttribute = (roofId, canvas, textMode) => {
|
||||
currentRoof = { roofLine }
|
||||
}
|
||||
})
|
||||
// offset=0(처마/케라바 출폭 0) 케이스에서 매칭 실패하면 wall.line 자체를 roofLine 으로 사용
|
||||
// (wall == roof 이므로 자기 자신이 맞음). currentRoof 가 undefined 일 때 NPE 방지.
|
||||
if (!currentRoof) {
|
||||
currentRoof = { roofLine: line }
|
||||
}
|
||||
} else {
|
||||
const findEdge = { vertex1: { x: midX, y: midY }, vertex2: { x: midX + roofVector.x * offset, y: midY + roofVector.y * offset } }
|
||||
const edgeDx =
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
@ -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, rawPoints)
|
||||
|
||||
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로부터 반환된 스켈레톤 객체
|
||||
@ -1015,7 +1111,7 @@ export const skeletonBuilder = (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)
|
||||
@ -1243,24 +1339,29 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver
|
||||
})
|
||||
}
|
||||
|
||||
// 각 라인 집합 정렬
|
||||
const sortWallLines = ensureCounterClockwiseLines(wallLines)
|
||||
// wall.baseLines를 독립적으로 CCW 정렬하면 ㅗ→붕괴 등으로 꼭짓점 집합이 달라질 때
|
||||
// 시작점(min-Y-then-min-X)이 wallLines와 어긋나 배열이 shift된다.
|
||||
// wallLines[i]와 wall.baseLines[i]는 초기 생성 시부터 raw-index 1:1 페어링이 유지되므로,
|
||||
// sortWallLines의 CCW 순서에 대응하는 원 인덱스를 복원하여 같은 순서로 wall.baseLines를 매핑한다.
|
||||
// [정렬 정책 — 인덱스가 생명] (2026-04-27)
|
||||
// 1) master = roofLines. roofLine 은 외곽(처마) 경계라 사용자 mutation 영향 없음 → 시작점 안정.
|
||||
// 2) ensureCounterClockwiseLines 가 master 의 leftTop(min-Y → min-X) 꼭짓점에서 시작해 CCW 정렬.
|
||||
// 3) wall.lines, wall.baseLines 는 raw 인덱스 1:1 페어링 유지된다는 전제로
|
||||
// master 의 sorted 순서 → 원 인덱스 → wall.lines[idx] / wall.baseLines[idx] 매핑.
|
||||
// wall 쪽을 master 로 쓰던 구현은 OVER 흡수/usePropertiesSetting 등에서 wall.lines 가
|
||||
// 재할당되며 시작점이 어긋나 페어 인덱스가 회전되던 문제(2026-04-27 ㅗ 좌측오버 케이스) 가 있어 변경.
|
||||
const _keyOfEdge = (l) => {
|
||||
const a = `${Math.round(l.x1 * 10) / 10},${Math.round(l.y1 * 10) / 10}`
|
||||
const b = `${Math.round(l.x2 * 10) / 10},${Math.round(l.y2 * 10) / 10}`
|
||||
return a < b ? `${a}|${b}` : `${b}|${a}`
|
||||
}
|
||||
const _rawIdxByKey = new Map()
|
||||
wallLines.forEach((l, i) => _rawIdxByKey.set(_keyOfEdge(l), i))
|
||||
const sortWallBaseLines = sortWallLines.map((sl) => {
|
||||
roofLines.forEach((l, i) => _rawIdxByKey.set(_keyOfEdge(l), i))
|
||||
const sortRoofLines = ensureCounterClockwiseLines(roofLines)
|
||||
const sortWallLines = sortRoofLines.map((sl) => {
|
||||
const origIdx = _rawIdxByKey.get(_keyOfEdge(sl))
|
||||
return origIdx != null && wallLines[origIdx] ? wallLines[origIdx] : sl
|
||||
})
|
||||
const sortWallBaseLines = sortRoofLines.map((sl) => {
|
||||
const origIdx = _rawIdxByKey.get(_keyOfEdge(sl))
|
||||
return origIdx != null && wall.baseLines[origIdx] ? wall.baseLines[origIdx] : sl
|
||||
})
|
||||
const sortRoofLines = ensureCounterClockwiseLines(roofLines)
|
||||
|
||||
// [MOVE-TRACE] 진입 스냅샷 (필요 시 주석 해제)
|
||||
// console.log('[MOVE-TRACE] === getMoveUpDownLine entry ===',
|
||||
@ -1366,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
|
||||
@ -1411,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1500,10 +1623,11 @@ const createInnerLinesFromSkeleton = (roofId, canvas, skeleton, textMode, isOver
|
||||
const roofLine = sortRoofLines[index]
|
||||
const wallBaseLine = sortWallBaseLines[index]
|
||||
|
||||
// [SHOULDER-ABSORBED] wall.baseLines[k] 가 이동 흡수로 zero-length(planeSize≈0)면
|
||||
// 해당 edge 는 흡수되어 사라진 상태. pair 생성 자체가 불필요(DIR_MISMATCH + eaveHelpLine 노이즈 유발).
|
||||
// 참고: 같은 파일 line 666 의 filter(planeSize > 0) 선례와 동일 패턴.
|
||||
if (!wallBaseLine || (wallBaseLine.attributes?.planeSize ?? Infinity) < 1) {
|
||||
// [SHOULDER-ABSORBED] wall.baseLines[k] 가 이동 흡수/OVER_EPS 클램핑으로 거의 zero-length 이면
|
||||
// 해당 edge 는 흡수된 상태. pair 생성 자체가 불필요(DIR_MISMATCH + eaveHelpLine 노이즈 유발).
|
||||
// 임계 = 10: useMovementSetting OVER_EPS=0.5 (canvas unit) 시 잔여 길이=0.5 → planeSize=5 (calcLinePlaneSize × 10).
|
||||
// 안전마진 두고 < 10 으로 차단. 정상 baseLine 은 보통 50+ 이므로 false positive 없음.
|
||||
if (!wallBaseLine || (wallBaseLine.attributes?.planeSize ?? Infinity) < 10) {
|
||||
console.log(`⏭️ [pair#${index}] SHOULDER_ABSORBED skip: wb=(${Math.round(wallBaseLine?.x1)},${Math.round(wallBaseLine?.y1)})→(${Math.round(wallBaseLine?.x2)},${Math.round(wallBaseLine?.y2)}) planeSize=${wallBaseLine?.attributes?.planeSize}`)
|
||||
return
|
||||
}
|
||||
@ -1692,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()
|
||||
@ -1801,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()
|
||||
@ -1947,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()
|
||||
@ -2054,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