qcast-front/docs/plans/2026-06-01-skeleton-roofline-edit-design.md
Jaeyoung Lee 45a3199fba fix: 용마루 비직교 형상 외곽 지붕선 소실 수정
[작업내용] :
- splitRoofLinesToInnerLines 의 hasOverlapLine 가드를 축정렬 bbox 포함 → 공선+구간 덮음으로 교체
- 육각형 등 잘린 모서리의 짧은 수평/수직 처마변(bbox 한 축 두께 0)이 대각선 추녀 bbox 에
  삼켜져 skip → 원본 외곽선 transparent 로 소실되던 false positive 제거
- 양 끝점이 모두 상대 선분 위(isPointOnLineNew)일 때만 overlap 인정 → 공선 아닌 추녀는 통과
- 변별/박공/한쪽흐름 경로 회귀 없음(직교 형상은 bbox 포함=공선 겹침이라 동작 불변)
- 설계 문서 §10 진단 결과·수정·회귀 분석 기록

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 13:07:20 +09:00

250 lines
21 KiB
Markdown

# 용마루(skeleton) 경로 외곽 지붕선 편집 기능 — 설계
- 작성일: 2026-06-01
- 대상 브랜치: `feature/roofline-edit`
- 관련 파일: `src/util/skeleton-utils.js`, `src/util/qpolygon-utils.js`, `src/hooks/useContextMenu.js`
## 1. 배경 / 문제
지붕 그리기는 `QPolygon.drawHelpLine()` 에서 외벽선 type 패턴으로 분기한다.
- **변별 경로** (`drawRoofByAttribute`, qpolygon-utils.js): 외곽선(`roof.lines`)을 내부선 끝점 기준으로 split 해서 추녀(hip)/지붕선을 `name='hip'` selectable QLine 으로 `roof.innerLines` 에 넣는다. 사용자는 이 라인을 select → 우클릭으로 편집할 수 있다.
- **용마루(skeleton) 경로** (`drawSkeletonRidgeRoof``skeletonBuilder``createInnerLinesFromSkeleton`): 외곽 처마선을 SK 빌더의 outer edge 로 만들되 `lineName='roofLine'` + `selectable:false` + `visible:false`**숨겨버린다** (`skeleton-utils.js:1611-1643`). 그래서 외곽 지붕선을 편집할 수 없다.
**목표**: 용마루 경로에서도 외곽 지붕선을 변별 경로와 **동일한 형태의 innerLine** 으로 생성하여, 기존 select + 우클릭 편집 기능을 그대로 재사용한다.
## 2. 핵심 사실 (탐색 결과)
### 2-1. innerLine 편집 기능의 정체
사용자가 innerLine 을 편집하는 통로는 **select → 우클릭 컨텍스트 메뉴**다. `useContextMenu.js:309-312` 에서 `auxiliaryLine` / `hip` / `ridge` / `eaveHelpLine` 을 한 case 로 묶는다.
| 메뉴 | 동작 |
|---|---|
| 사이즈 변경 | `AuxiliarySize` 모달 |
| 보조선 이동(M) / 복사(C) | `AuxiliaryEdit` 모달 |
| 보조선 삭제 / 전체삭제 | `roof.innerLines` 에서 제거 |
| 수직 이등분선 | `roof.innerLines.push` |
이 분기는 **`currentObject.name` 기준**이다. 따라서 외곽 지붕선을 `name='hip'` (또는 `'ridge'`) 로 만들어 innerLine 에 넣기만 하면 이 메뉴가 **자동으로** 붙는다. 별도 바인딩 코드 불필요.
### 2-2. 변별 경로는 이미 동작 중 (= 목표 동작의 레퍼런스)
변별 경로 split 은 외곽 지붕선을 `drawRoofLine` 으로 그리는데, 그 함수의 `name``SUBLINE.HIP` 이다 (의도적 재사용). 즉 변별 경로의 외곽 지붕선은 이미 `name='hip'` innerLine 으로 들어가 select+우클릭이 동작한다. **용마루도 이와 동일하게 동작하면 된다 (전용 차이 없음).**
### 2-3. innerLine 이 편집 가능하려면 가져야 하는 속성
| 조건 | 값 | 안 맞으면 |
|---|---|---|
| `type` | `'QLine'` | 플랜 복원/필터 누락 (`useCanvasConfigInitialize.js:243`) |
| `name` | `'hip'` / `'ridge'` (SUBLINE 값) | 컨텍스트 메뉴·이동 분기에 안 걸림 |
| `attributes.roofId` / `parentId` | `roof.id` | roof 조회·자식 이동 실패 |
| `selectable` | `true` | 클릭 자체 불가 |
| **금지** | `name='roofLine'`, `selectable:false`, `evented:false` | 현재 숨긴 외곽선이 정확히 이 상태 |
### 2-4. 재그리기 함정
`drawHelpLine()` 은 호출될 때마다 `roof.innerLines` 를 비우고 SK 함수가 처음부터 다시 그린다 (`QPolygon.js:582-598`). 따라서 외곽 지붕선을 "한 번 추가" 하는 것으로 부족하고, **재그리기 때마다 skeleton 경로가 자동 재생성** 하도록 만들어야 살아남는다.
## 3. 설계 (접근안 A: split 로직 공용 추출)
### 3-1. 공용 함수 추출
`qpolygon-utils.js:5432-5496` 의 "지붕선에 따라 라인추가" 블록을 함수로 추출:
```
splitRoofLinesToInnerLines(roof, canvas, textMode) → QLine[]
- innerLinesPoints 수집 (innerLines 끝점 unique)
- roof.lines 순회:
hasOverlapLine 가드 (innerLine 이 외곽선을 완전 포함하면 skip)
splitPoint 수집 (innerLinesPoints 중 currentLine 위 + 끝점 아닌 점)
분류: 첫 구간 = drawHipLine(prevDegree)
중간 구간 = drawRoofLine
끝 구간 = (splitPoint 1개면 drawRoofLine, 아니면 drawHipLine(nextDegree))
splitPoint 없으면 통째로 drawRoofLine
- 생성 라인을 innerLines 에 push 후 반환
```
- **위치**: `drawHipLine` / `drawRoofLine` 이 있는 `qpolygon-utils.js` 에 두고 `export`. `skeleton-utils.js` 에서 import.
- **정렬 버그 동시 수정**: 현재 `splitPoint.sort((a, b) => a[1] - b[1])` 는 원소가 `{point, distance}` 객체라 `a[1]``undefined` → 정렬 no-op. `a.distance - b.distance` 로 수정. (분할점 2개 이상 케이스에서 라인이 거리순으로 그려지지 않던 잠재 버그)
- **`drawRoofByAttribute` 교체**: 인라인 블록을 함수 호출로 교체. 순수 추출이므로 변별 경로 동작 불변.
### 3-2. skeleton 경로 변경
- `createInnerLinesFromSkeleton``isOuterEdge`(SK eaves) 숨김 처리는 **유지**. (§9 정정: SK eaves 라인은 baseLine 기준이라, 노출하면 처마변 split 결과와 offset 만큼 어긋나 이중 표시됨. 처마 외곽선은 아래 split 으로 별도 innerLine 화.)
- **추녀 연장(`drawBaselineToRooflineHelpers`, `:1285`) 완료 후** `splitRoofLinesToInnerLines(roof, canvas, textMode)` 호출 → 추녀 종점이 처마변 위에 있는 상태라 split 정합 성립, 외곽 지붕선이 변별 경로와 동일한 형태로 `roof.innerLines` 에 합류. (⚠️ 연장 **전**에 호출하면 종점이 baseLine 위라 분할점을 못 잡음 — §9 참조)
- `drawHelpLine` 재호출 시마다 자동 재실행되어 편집 후 재그리기에도 외곽 지붕선 유지.
## 4. 리스크 & 구현 전 검증
1. **SK 추녀 끝점 ↔ `roof.lines` 정합성** — ✅ **검증 완료 (2026-06-12, §9 참조)**: **연장 후 정합 OK**. SK 골조는 `wall.baseLines` 기준이라 추녀 끝점이 처음엔 baseLine 코너에 있으나(연장 전 `onRoof=false`), `drawBaselineToRooflineHelpers`(`:1285`)가 추녀를 처마변까지 **연장**해 종점을 처마변 위로 옮긴다(연장 후 4코너 전부 `diffRoof=0`, `onRoof=true`). → **split 을 연장 다음에 호출하면 정합 자동 성립**. (초기 측정이 연장 전에 찍혀 "구조적 불일치"로 오판했음 — §9-0 참조)
2. **순환 import**: `qpolygon-utils ↔ skeleton-utils` 상호 import 여부 점검. 순환이면 split 함수를 제3의 모듈(예: canvas-util)로 추출하거나 의존성 정리.
3. **기존 `isOuterEdge` roofLine 제거 영향**: 숨긴 라인을 참조하는 곳(치수 계산, 플랜 저장/복원 등)이 있는지 확인 후 제거.
## 5. 성공 조건 (검증 기준)
- 용마루 지붕에서 외곽 지붕선이 화면에 보이고 클릭 선택된다.
- 우클릭 시 hip/ridge 와 동일한 메뉴(사이즈변경/이동/복사/삭제/수직이등분선)가 노출·동작한다.
- 마루이동 등으로 재그리기 후에도 외곽 지붕선이 유지된다.
- 변별 경로 동작 회귀 없음 (외곽선 split 결과 동일, 정렬 버그 수정분 제외).
## 6. 범위 밖 (YAGNI)
- 외곽 지붕선 전용 우클릭 메뉴/가드 (변별 경로와 완전 동일하게 가므로 추가 분기 없음).
> 박공(`drawGableRoof`) / 한쪽흐름(`drawShedRoof`) 경로는 **§7 에서 범위로 편입** (2026-06-09).
---
## 7. 4경로 종합 (2026-06-09 추가)
`QPolygon.drawHelpLine()`(`QPolygon.js:773-791`) 의 4갈래 분기별 외곽 지붕선 처리 현황.
| 경로 | 함수 | 외곽선 생성 방식 | name / selectable / visible | 편집 가능? | 작업량 |
|---|---|---|---|---|---|
| **변별** | `drawRoofByAttribute` (`qpolygon-utils.js:1731`) | `roof.lines` 직접 순회 → innerLine 끝점으로 **split** (`:5432-5496`) | `hip` / true / true | ✅ 레퍼런스 | 없음(원본 추출) |
| **박공** | `drawGableRoof` (`:710`) | 지붕면 **둘레 재구성**`drawRoofLine`/`drawHipLine` (`:1605-1617`), `roof.innerLines.push` (`:1630`) | `hip` / true / true | ✅ **이미 됨** | 회귀 검증만 |
| **용마루** | `drawSkeletonRidgeRoof``createInnerLinesFromSkeleton` | SK outer edge QLine 직접 생성 후 **숨김** (`skeleton-utils.js:1624,1638`) | `eaves` / **false** / **false** | ❌ | 중 (§3) |
| **한쪽흐름** | `drawShedRoof` (`:1644`) | 외곽선 innerLine화 **없음**`pitchSizeLine` 만 add (`:1701-1721`) | — | ❌ | 대 (§8) |
핵심 계약: `drawRoofLine`(`:5863`) 과 `drawHipLine`(`:5763`) 은 둘 다 `name: LINE_TYPE.SUBLINE.HIP` + `selectable` 미지정(QLine 기본 true). 즉 **"이 함수로 그린 외곽선 = 편집 가능한 hip innerLine"**. 컨텍스트 메뉴는 `useContextMenu.js:300-303` 에서 `name` 기준으로 자동 연결되므로 별도 바인딩 불필요(§2-1 과 동일).
### 7-1. 박공 — 이미 편집 가능, 메커니즘만 다름
외곽선이 `name='hip'` innerLine 으로 이미 들어가 select+우클릭 동작. 단 변별처럼 `roof.lines` 를 split 하지 않고 **지붕면 둘레 점을 직교 재정렬**(`getSortedOrthogonalPoints`, `:1571`)해서 그린다. → "신규 구현" 이 아니라 **이동/사이즈변경 시 `roof.lines` 정합성 회귀 검증** 대상.
### 7-2. 한쪽흐름 — 미구현 + 경사변 특수성 (→ §8)
`drawShedRoof``roof.innerLines` 를 건드리지 않아 초기화된 `[]` 로 남는다. 외곽선 편집을 위해 split 을 새로 붙여야 하나, **공용 split 을 그대로 쓰면 경사변의 실제길이가 틀린다** — §8.
## 8. 한쪽흐름 degree 전략 주입 설계 (2026-06-09 추가)
### 8-1. 문제: 평면길이 ≠ 실제길이
한쪽흐름은 흐름방향으로 기울어져 변마다 경사 적용 여부가 다르다.
| 변 종류 | 흐름방향 관계 | 경사 | plane vs actual |
|---|---|---|---|
| 처마(eaves) / 마루(shed) | 수직 | 수평 | plane = actual |
| **박공(gable)** | **평행** | 경사면 | **plane ≠ actual** (= `planeSize / cos(shedDegree)`) |
공용 split 이 외곽선을 그리는 `drawRoofLine`(`:5863`) 은 `actualSize: calcLinePlaneSize(...)`**무조건 actual=plane** 을 박는다(degree 인자 없음, 수평선 전용 가정). `drawHipLine`(`:5763`) 은 degree 를 받지만 split 이 넘기는 값은 **인접 변의 `attributes.pitch`**(`:5443-5444`)라 흐름 경사와 무관. 게다가 한쪽흐름은 분할점이 없어(내부 용마루·추녀 없음) 전 외곽변이 통째 `drawRoofLine` 으로 그려져 → 박공변도 actual=plane → **틀림**.
도메인 규칙의 단서는 `drawShedRoof` 자체에 이미 있다(`:1656-1669`):
```js
const shedDegree = getDegreeByChon(sheds[0].attributes.pitch) // 흐름 경사각
gables.forEach((gable) =>
gable.attributes.actualSize = calcLineActualSize({...gable}, shedDegree)) // 박공변만 보정
```
`calcLineActualSize(points, deg) = planeSize / cos(deg)` (`:6270`). 현재는 `roof.lines` attributes 에만 쓰고 innerLine 을 안 만들어 편집 불가.
### 8-2. 설계 (방향 1 채택): degree 결정 전략 주입
공용 split 의 **기하(분할/연결) 로직은 재사용**, "각 분할 라인의 degree" 만 경로별 전략으로 분리한다.
`splitRoofLinesToInnerLines(roof, canvas, textMode, opts)` 시그니처에 degree 전략 주입:
```
opts.degreeOf?: (parentLine) => number // 부모 외곽변 → 경사각(도). 없으면 기존 인접 pitch 방식.
```
- **변별 / 박공 / 용마루**: `degreeOf` 미전달 → 기존 끝점·인접 pitch 기반 (동작 불변).
- **한쪽흐름**: `degreeOf = (parentLine) => isGable(parentLine) ? shedDegree : 0`
- `isGable`: 부모 변의 `attributes.type === LINE_TYPE.WALLLINE.GABLE` (흐름방향 평행 변).
구간 그리기 분기:
- 경사 구간(`deg > 0`): `drawRoofLine` 대신 **`drawHipLine(points, …, deg, deg)`**. 박공변이 수평/수직이면 `drawHipLine` 의 대각선 분기(`:5767`)를 건너뛰고 `calcLineActualSize(points, deg)``plane/cos(deg)` 계산 → `drawShedRoof` 기존 보정과 **수식 일치**.
- 수평 구간(`deg === 0`): 기존대로 `drawRoofLine`.
### 8-3. 편집 환산까지 해결 — degree 를 라인 attributes 에 저장
표시용 actualSize 뿐 아니라 **select → 사이즈 변경** 시 입력값(plane↔actual) 환산에도 라인이 자기 degree 를 들고 있어야 한다. 변별/용마루 hip 은 `attributes` 에 경사 정보가 있어 환산되지만, 한쪽흐름 경사변은 **`shedDegree` 를 라인 `attributes.pitch`(또는 degree)에 심지 않으면 편집 모달 환산이 깨진다.** → §8-2 의 `drawHipLine(…, deg, deg)` 호출이 degree 를 라인 속성에 함께 저장하도록 하여 표시·편집을 동시에 해결.
### 8-4. 적용 순서 / 검증
1. `splitRoofLinesToInnerLines` 추출 시 `degreeOf` 옵션 포함 (§3-1 + §8-2 병합).
2. `drawShedRoof` 끝에서 `splitRoofLinesToInnerLines(roof, canvas, textMode, { degreeOf })` 호출. 기존 `pitchSizeLine` 추가는 유지.
3. 검증: 한쪽흐름 박공변 select 시 actual 표시값 = `plane / cos(shedDegree)`, 사이즈변경 모달에서 plane↔actual 양방향 환산 정상, 처마/마루변은 plane=actual.
4. 회귀: 변별·박공·용마루는 `degreeOf` 미전달이므로 동작 불변 확인.
### 8-5. 리스크
- **분할점 부재**: 한쪽흐름은 내부 보조선이 없어 외곽변이 1:1 통짜 라인으로 그려진다 → 부모 변 type 매칭이 단순(분할 시에도 조각은 부모 type 상속). split 의 끝점 정합성(`isPointOnLineNew`, tol 0.1) 자체는 분할점이 없으면 무의미하므로 `else` 통짜 경로(`:5493`)로 안전하게 떨어진다.
- **`isGable` 판정원**: `drawShedRoof``gables` 분류(`:1653`)와 동일 기준(`attributes.type === GABLE`)을 써야 보정 대상이 일치.
---
## 9. SK 끝점 ↔ 처마변 정합 — 실측 결과 (2026-06-12, 측정 시점 정정)
§4 리스크1 을 임시 계측(`skeleton-utils.js``logSkRoofLineAlignment`)으로 추녀 연장 **전/후** 2단계 실측한 결과.
### 9-0. ⚠️ 측정 시점 정정 (중요)
1차 측정은 `createInnerLinesFromSkeleton` **직후**(`:1279`, 추녀 연장 전)에 찍혀, 추녀 종점이 baseLine 코너에 있는 상태(`onRoof=false`, `perpRoof=offset`)만 보고 **"구조적 불일치 → 투영 보정 필요"로 오판**했다. 실제로는 그 직후 `drawBaselineToRooflineHelpers`(`:1285`)가 추녀를 처마변까지 연장한다. 연장 **후**(`afterExt`) 측정에서 정합이 성립함을 확인 → 아래는 정정된 결론. **(투영 보정·SK 입력 교체·연장 보정 설계는 모두 폐기. 불필요했음.)**
### 9-1. 실측 결과 (용마루 우진각, 변별 offset 50px)
처마변 x∈[357.5,1147.6]·y∈[115.2,636.7] vs baseLine x∈[407.5,1097.6]·y∈[165.2,586.7] (전 변 50px 안쪽).
| 추녀 끝점 | beforeExt (`:1279`, 연장 전) | afterExt (`:1285`, 연장 후) |
|---|---|---|
| **바깥** 끝점(4코너) | baseLine 코너 — `onRoof=false`, `diffRoof=22.4`, `onBase=true` | **처마 코너로 연장 — `onRoof=true`, `diffRoof=0`** |
| 안쪽·용마루(내부점) | `onRoof=false` (260.7) | `onRoof=false` (260.7) — 내부점이라 정상 |
예: 추녀 바깥 끝 `(407.5,165.2)`(baseLine) → `(357.5,115.2)`(처마 코너) 로 연장. 4코너 전부 `diffRoof=0`, `onRoof=true`.
### 9-2. 추녀 연장 메커니즘 (이미 구현됨)
`drawBaselineToRooflineHelpers`(`:685`, 플래그 `DRAW_BASELINE_TO_ROOFLINE_HELPER=true`, `:25`):
- `hipLines` 추출(`:734`) → baseLine 코너에 닿는 hip 만(`CORNER_EPS`, `:773`) → 추녀 진행 방향으로 ray cast(`rayHitSegment`, `:693`) → 처마변 교점까지 hip 좌표를 **연장**(`:861-865`).
- `actualSize` 비율 보정(`:871`) + `__extended`/`attributes.extended` 플래그로 save/load 영속(`:879`).
- 즉 "추녀는 roof.lines 까지 연결되는 선" 이 이미 코드에 있다. SK 골조 자체는 baseLine 기준(동기화 유지) + 추녀 종점만 처마변까지 연장.
### 9-3. 결론 — split 은 추녀 연장 **다음**에 호출
- 추녀 종점은 연장 후 처마변 위(`onRoof=true`, `diffRoof=0`)에 정확히 앉으므로, **split/외곽선 innerLine화의 정합은 깨지지 않는다.**
- **유일한 요건**: `splitRoofLinesToInnerLines` 호출을 `createInnerLinesFromSkeleton` 직후가 아니라 **`drawBaselineToRooflineHelpers`(`:1285`) 다음에** 둘 것. §3-2 가 호출 위치를 연장 전으로 잡은 것이 유일한 함정이었음.
- **단순 우진각**: 추녀가 처마 **코너**(변 끝점)에 닿으므로 split 분할점 없음 → 처마 4변 통짜 innerLine(정상).
- **복합 형상**(L자·凸자): 추녀가 처마변 **중간**에 닿으면 그 점에서 분할. 연장 후 종점이 처마변 위이므로 분할점으로 정상 인식.
### 9-4. 검증 기준 (구현 시)
- split 을 연장 후 호출 → 처마 4변이 innerLine 으로 등록, select+우클릭 편집 가능.
- 복합 형상: 처마변 중간 접점에서 분할, 각 조각 독립 편집.
- offset=0 / offset≠0 양쪽 동작.
- 마루이동/벽이동 후 재그리기에도 유지(연장은 `__extended`/`attributes.extended` 로 영속).
- **임시 계측(`logSkRoofLineAlignment` 정의 + 호출부 3곳 `:1279`/`:1285`/`:1413`) 제거** 후 커밋.
---
## 10. 육각형 등 비직교 형상에서 외곽 지붕선 소실 — 진단 (2026-06-22 추가)
### 10-1. 증상
**육각형(wall 6변) 용마루 지붕** 설정 시 오른쪽 위 외곽 지붕선 2개가 화면에서 사라진다.
단순 우진각(사각형)은 정상(§9-1 실측 검증 완료) — 비직교(대각선) 변이 많은 형상에서만 발생.
### 10-2. 가설 (코드 분석, 1순위)
`splitRoofLinesToInnerLines`(`qpolygon-utils.js:5485`) 는 처마변을 innerLine 으로 등록한 뒤
**roof 원본 외곽선을 `stroke:'transparent'` 로 숨긴다**(`:5563`). 따라서 어떤 처마변이
innerLine 으로 **재등록되지 못하면** 원본 대체선도 없어 그 자리에 선이 완전히 사라진다.
누락 1순위 지점 = `hasOverlapLine` 가드(`:5511-5523`). 이는 **축정렬 bounding-box 포함관계**로만
overlap 을 판정하는데, 이 블록은 원래 **변별 경로(`drawRoofByAttribute`, 주로 직교 형상)**
인라인 코드를 `1bf5f45b` 에서 공용 추출한 것이다. 직교 변은 bbox 가 선분에 딱 맞아 false positive 가
드물지만, **육각형의 대각선 처마변 + 대각선 추녀(hip)** 는 bbox 가 큰 사각형이라 코너를 공유할 때
서로의 bbox 를 포함하기 쉬워 `hasOverlapLine=true` 오발동 → 해당 처마변 `return`(skip).
오른쪽 위 짧은 대각선 변 2개가 인접 추녀 bbox 에 함께 포함되어 동시 skip 된 것으로 추정.
### 10-3. 진단 결과 — 가설 확정 (2026-06-22)
육각형(직사각형 우상단 모서리를 계단으로 자른 형상) 재현 `[HEXDIAG]` 로그:
split entry roof.lines=6 innerLines=9
line#0 WHOLE (386,35)→(386,1335) ← 좌 수직변, 정상
line#1 WHOLE (386,1335)→(1286,1335) ← 하 수평변, 정상
line#2 WHOLE (1286,1335)→(1286,235) ← 우 수직변, 정상
line#3 SKIP (1286,235)→(1086,235) overlapBy=inner⊇curr hip(1286,235)→(836,685) ✗
line#4 SKIP (1086,235)→(1086,35) overlapBy=inner⊇curr hip(1086,35)→(736,385) ✗
line#5 WHOLE (1086,35)→(386,35) ← 상 수평변, 정상
- 사라진 line#3(수평, y두께 0)·line#4(수직, x두께 0) = 잘린 계단의 짧은 변 2개.
- 둘 다 `inner⊇curr`: 끝점 코너를 공유한 **대각선 추녀(hip)의 bbox 가 0-두께 처마변 bbox 를 포함**.
예) line#3 bbox `x[1086,1286]·y[235,235]` ⊂ hip bbox `x[836,1286]·y[235,685]`.
- line#0·1·2·5 가 멀쩡한 건 길어서 어떤 hip bbox 로도 통째로 못 감싸기 때문. → **가설 확정.**
- 1순위 정정: 범인은 "대각선 처마변"이 아니라 **축정렬(수평/수직) 짧은 처마변 + 대각선 추녀** 조합.
### 10-4. 수정 (적용 완료, 2026-06-22)
`hasOverlapLine` 판정을 **bbox 포함 → 공선 + 구간 덮음**으로 교체(`qpolygon-utils.js:5507-5519`):
한쪽 선분의 **양 끝점이 모두 상대 선분 위**(`isPointOnLineNew`, tol 0.1)일 때만 overlap 인정.
대각선 추녀는 처마변과 공선이 아니라 통과 → 처마변 정상 등록. `[HEXDIAG]` 계측·`logger` import 전량 제거.
**회귀 영향 분석** (이 함수를 쓰는 4경로):
- **변별**(`drawRoofByAttribute``:5447`): 가드 원사용처. 원래 bbox 가 잡던 "진짜 같은 선 위 겹침"은 공선이라 새 판정으로도 잡힘. bbox-만-겹침(공선 아님)은 원래 false positive → 제거가 곧 개선. 회귀 위험 = 공선인데 부동소수로 tol 0.1 초과하는 경계뿐(드묾).
- **한쪽흐름**(`drawShedRoof``:1731`): 내부 보조선 없어 `innerLines` 거의 비어 가드 트리거 자체가 드묾. 영향 미미.
- **용마루**(skeleton `:1288`): 대상 케이스. 사각형 우진각·육각형 양쪽 확인.
- **박공**(`drawGableRoof`): split 미사용 → 무영향.
**재현 검증 체크리스트**: ① 육각형 용마루 6변 전부 등록(소실 없음) ② 단순 사각형 우진각 4변 유지(회귀 없음) ③ 변별 지붕 외곽선·추녀 정상 ④ 한쪽흐름 경사변 actual=plane/cos 유지. (캔버스 인터랙션이라 런타임 확인은 재현 필요 — 정적 분석상 ①만 동작 변화, ②③④ 불변 예상.)