qcast-front/.claude/memory/project_drawRoofByAttribute_internals.md
Jaeyoung Lee 4644269852 docs: 지붕 생성 로직 분석 메모리 문서 추가
qpolygon-utils 지붕 생성 구조 / drawRoofByAttribute 내부 구조 /
skeleton-utils 용마루 두 경로 차이 분석. roofline-edit 구현 시 reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 09:52:17 +09:00

397 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: drawRoofByAttribute 내부 구조 (후반부 상세)
description: qpolygon-utils.js drawRoofByAttribute (1680-5455, 3777줄) 의 단계별 정밀 분석. 라인 분류→가선분(linesAnalysis) 누적→MAX_ITERATIONS 교점 처리→하단지붕 파생→최종 split. 수정 시 어느 stage 인지 식별 필수.
type: project
---
# drawRoofByAttribute 내부 구조 (후반부 상세)
`src/util/qpolygon-utils.js:1680-5455` 의 정밀 분해. catch-all 함수라 모든 비표준 케이스(요세무네/맨사드/형이동/벽취합/팔작/반절처/케라바 혼합 등)가 여기 모인다.
## 0. 데이터 흐름 한 장 요약
```
wall.baseLines → [zero merge] → baseLines (mutated copy)
type 분류: sheds / hipAndGables / eaves / jerkinHeads / gables
[stage1 처리] 각 type 별로:
- innerLines.push(drawHip/Ridge/RoofLine ...) ← 즉시 그림
- linesAnalysis.push({start, end, left, right, type, degree}) ← 가선분 누적
[stage2 MAX_ITERATIONS 루프] linesAnalysis 끼리 교점 → drawHip/Ridge + newAnalysis 분기
[stage3 가선분 잔여] 평행 라인 예외처리
[stage4 벽취합 잔여] 벽-벽 라인 처리
[stage5 하단지붕 파생] downRoofGable (A/B) + downRoofEaves
[stage6 최종 split] roof.lines × innerLinesPoints 분할 → drawHip/RoofLine
roof.innerLines = innerLines (덮어쓰기)
```
`innerLines``linesAnalysis`**다른 컨테이너**:
- `innerLines`: 실제 캔버스에 그려진 QLine 객체들 (drawHip/Ridge/RoofLine 반환값)
- `linesAnalysis`: 가상 선분 메타데이터 (`{start, end, left, right, type, degree, gableId?, connectCnt?}`) — 아직 캔버스에 안 그려짐, 교점 계산용
`TYPES = { HIP, RIDGE, GABLE_LINE, NEW }` 는 linesAnalysis 내부 enum (canvas 의 `LINE_TYPE.SUBLINE` 과 별개).
## 1. 전반부 (1680-1995) — 이미 파악된 영역
- **1680-1759 형이동 zero-merge**: `wall.baseLines``planeSize<EPSILON` 인덱스를 prev/next 페어로 모아 같은 vector 라인 끼리 합쳐 `baseLines` 재구성. zero-merge 후엔 `wall.baseLines` 의 중간 라인이 사라진 짧은 형태가 됨.
- **1761 `checkWallPolygon`**: `baseLines` 의 시작점만 모아 만든 임시 QPolygon. inPolygon 판정 전용.
- **1766-1995 벽취합 corner extension**: WALL+offset>0 라인의 prev/next 처리, `addRoofLine1/2` 추가. `name='addRoofLine'`, `attributes.type=WALLLINE.ETC`.
## 2. analyzeLine 헬퍼 (2000-2133)
drawGableRoof 의 `analyzeEavesLine` 과 거의 동일하지만 모든 type 에 적용. **return**:
```
{ startPoint, endPoint, length, angleDegree, normalizedAngle,
isHorizontal, isVertical, isDiagonal,
directionVector: {x,y},
roofLine: 매칭된 roof.lines 의 한 라인,
roofVector: {x,y} (지붕 안쪽 방향, 단위벡터) }
```
핵심:
- tolerance 1° 로 H/V/Diagonal 분류
- `roof.lines` 중 directionVector 가 같은 것을 `checkRoofLines` 로 거름
- offset=0: 좌표 직접 매칭. 실패 시 `currentRoof = { roofLine: line }` 자기 자신 fallback (NPE 방지)
- offset>0: midPoint 에서 roofVector 방향으로 offset 만큼 가선 그어서 `edgesIntersection` 검색, intersect 길이 - offset < 0.1 으로 best match 선택
`roofLine` 매칭 실패 `currentRoof.roofLine` 직접 접근 (2130) fallback 로직이 있어서 안전하지만 추가 변경 NPE 주의.
## 3. getRidgeDrivePoint 헬퍼 (2142-2270)
마루(ridge)/이등분선 등이 **반대편 케라바(GABLE) 또는 이중처마(EAVES-EAVES) 코너**까지만 뻗도록 클램프하는 포인트를 계산.
- prevLine/nextLine 모두 EAVES 아니면 `null` (호출처에서 fallback 처리)
- prevLine EAVES 이고 prev GABLE 이면 GABLE roofLine testPoint 교점 (=GABLE 코너)
- prevLine EAVES 이고 prev EAVES 이면 EAVES 사잇각 vector hip edge 그어서 testPoint 와의 교점
- nextLine 쪽도 대칭
- 있으면 가까운
- prevHasGable / nextHasGable 우선순위: `prevLength <= nextLength` prev, 아니면 next GABLE 사용
호출처: hipAndGables, ridgeEaves, jerkinHeads, MAX_ITERATIONS 루프(NEW 가선분 분기 )
## 4. 라인 분류 (2275-2300)
```
baseLines.forEach(switch on attributes.type):
SHED → sheds[]
HIPANDGABLE → hipAndGables[] (팔작지붕/이리모야)
EAVES → eaves[] (처마)
JERKINHEAD → jerkinHeads[] (반절처)
GABLE → gables[] (케라바/박공)
default → 무시 (WALL, ETC, DEFAULT 등)
```
`linesAnalysis = []` 초기화 (2303).
## 5. 단계별 처리 (1~5)
### 5-1. sheds.forEach (2306-2467) — 한쪽흐름
가드: 양옆이 GABLE 이어야 . 아니면 return.
1. 맞은편 EAVES 라인 찾기 shedDegree 산출 (없으면 4寸 default)
2. roofLine 끝점에서 지붕 안쪽으로의 vector (`checkRoofVector1/2`) 그어서 반대편 roof.lines 와의 교점 (`intersectPoints1/2`)
3. **HIP 가선분 2개 push** ( 끝점 교점), `left=beforePrevIndex/nextIndex`, `right=prevIndex/afterNextIndex`, `type=TYPES.HIP`, `degree=shedDegree`
`intersectPoints1[0]` 직접 인덱스 접근 (2441) sort 비어있으면 NPE.
### 5-2. hipAndGables.forEach (2470-2699) — 팔작지붕
가드: 양옆이 EAVES. 그리고 prevLine/nextLine vector 같으면(같은 방향, U자) silent return.
1. `lineWidth = min(currentLine.attributes.width, roofLength/2)` 팔작지붕 두께 클램프
2. prevRoofLine/nextRoofLine currentRoofLine 사잇각 vector prevHipPoint/nextHipPoint 산출 (지붕 안쪽 방향 정규화)
3. **HIP 2개 즉시 그림** (`innerLines.push(drawHipLine prev, next)`)
4. 양옆이 모두 EAVES (gable 짧을 ): midPoint 에서 양쪽 gable hip 두개 추가 + roofVector 반대 방향으로 10000 길이 가선 그어서 반대편 roof 교점 `getRidgeDrivePoint` 클램프 `oppLine.attributes.type` offset 보정 (HIPANDGABLE=width, JERKINHEAD=width/2, WALL=0) **RIDGE 가선분 push**
5. else (gable 길이 충분): `drawRoofLine(gablePoint)` 즉시 그림. 길이 ~0 이면 RIDGE 가선분만 push
### 5-3. eaves 처리 (2701-3143)
eaves `planeSize` 오름차순 sort 그룹으로 분리:
- **`ridgeEaves`** (2704-2723): 양옆 모두 EAVES + prev/next vector 다른 방향 + checkWallPolygon.inPolygon(midPoint+nextVector) 처마-처마-처마 코너에서 안쪽 들여진 모양
- **`hipedEaves`** (2725): 나머지 (= 옆이 GABLE )
#### ridgeEaves.forEach (2731-3036) — 가장 복잡한 블록
상태 누적기:
- `proceedEaves[]`: `{prev, current, point: {x1,y1,x2,y2}, length}` 처리된 처마 hip
- `proceedRidges[]`: `{prev, next, point}` 처리된 ridge
알고리즘:
1. pHipVector/nHipVector ( 옆과의 사잇각) 산출, inPolygon 으로 정규화
2. `findRoofPoints(checkPoint)` 헬퍼: hipEdge roof.lines 교점들을 forward/backward 분리 가까운 선택. forward+backward / backward 2개 / forward 2개 fallback
3. prevHipPoint/nextHipPoint hip `edgesIntersection` 으로 정점 보정
4. **proceedRidges 루프**: 같은 base index 영향권의 ridge prev/next hip 교차하면 `isRidgePrev/isRidgeNext` 플래그 hip 좌표를 forward 되돌림(roof 정점까지)
5. **proceedEaves 중복 처리**: `alreadyPrev/Next` 있으면 길이가 짧은 쪽으로 교체 (또는 hip 좌표를 기존 것으로 덮음). isRidge 플래그 무조건 교체
6. prevHipPoint.x2 nextHipPoint.x2 같으면(=정점 만남) **ridge 생성**:
- midPoint 에서 ridgeVector(nextLine 방향, inPolygon 정규화) 가선
- roof.lines 와의 가까운 교점 ridge 끝점
- `getRidgeDrivePoint` 클램프
- `isOverlapBefore/After` (beforePrev/afterNext currentLine 동일 좌표 범위) oppLine.type offset 보정 분기:
- 양옆에 GABLE 있음(otherGable): drivePoint 우선
- 없음 + oppLine=EAVES + 양옆에 EAVES: 길이 같으면 drivePoint, 다르면 offset
- 없음 + 외: offset 적용
7. ridge 중복 체크 `proceedRidges.push`
#### hipedEaves.forEach (3038-3143)
가드: prevLine EAVES 여야 . 아니면 return.
1. currentLine prevLine 사잇각 vector hipVector (inPolygon 정규화)
2. `currentLine.x1` 에서 hipVector × `analyze.length` 길이로 가선 roof.lines forward/backward 교점
3. **HIP 가선분 push** (linesAnalysis), `degree=currentDegree` (currentLine.pitch)
#### proceedEaves → linesAnalysis 변환 (3146-3219)
```
proceedEaves.forEach((eaves, index)):
- jointEaves = 같은 끝점(point.x2,y2) 공유하는 다른 eaves 검색
- jointEaves.length === 1 + ridgeEaves.length > 1:
drawHipLine 즉시 그림 (innerLines.push)
- jointEaves.length === 2 (3-way 코너):
jointIndex 3개 모두 drawHipLine 즉시
proceedRidges 에서 해당 코너 ridge 제거
"denyVector" (반대쌍 없는 vector) 찾아서 roof 와의 교점 → HIP 가선분 push (uniqueLine 좌우)
- else:
HIP 가선분 push (linesAnalysis)
```
#### proceedRidges → linesAnalysis (3220-3229): 모두 RIDGE 가선분으로 push
### 5-4. jerkinHeads.forEach (3232-3445) — 반절처
가드: 양옆 EAVES. 양옆 vector 같으면 silent return.
1. `gableWidth = currentLine.attributes.width`, `lineLength = (roofLength - gableWidth) / 2`
2. roofLine 따라 4개 (`jerkinPoints`): 시작 +lineLength +gableWidth +lineLength=
3. `gableWidth >= roofLength`: **drawRoofLine** 즉시 (roofPoints 그대로)
4. 정상: **drawHipLine(gable1, prevDegree) + drawRoofLine(gable2) + drawHipLine(gable3, nextDegree)** 즉시
5. 추가로 prevRoofLine/nextRoofLine 과의 사잇각 hipVector × gableWidth prevHipEdge/nextHipEdge 교점 hipIntersection
6. hipIntersection 있으면 **drawHipLine prev/next 2개 즉시** + roofVector 반대 방향으로 10000 가선 roof 교점 drivePoint 클램프 offset 보정 **RIDGE 가선분 push**
### 5-5. gables.forEach (3447-4239) — 케라바/박공 (가장 큰 블록)
가드: 양옆 EAVES. 그리고 vector 다른 방향 + checkWallPolygon.inPolygon 통과해야 처리.
#### isOverlap 분기 (3502-3562)
`isOverlapBefore/After` (beforePrev 또는 afterNext currentLine 좌표 범위 동일) + otherGable 없음 **isOverlap=true**:
- mid-to-mid ridge: cMid + ridgeVector*cOffset oMid - ridgeVector*oOffset, oppLine.type 추가 offset
- 중복 체크 **RIDGE 가선분 push**
#### 일반 분기 (3563-4182)
stdLine 결정: prevLine/nextLine 거리 (`prevDistance/nextDistance`) 비교, GABLE 이면 , 아니면 짧은 . `stdFindOppVector` 같이 결정.
`stdPoints` 좌표 보정:
- **stdPrevVector === stdNextVector** (U자 코너): isHorizontal/isVertical × stdVector × prev/next y/x 비교로 8가지 분기, GABLE 때만 offset 적용
- 다른 방향: 끝점에 prev/nextLine offset 적용
`oppositeLine` 검색: stdLine 반대편 (stdFindOppVector 방향) 평행 라인들 (overlap 케이스 포함)
`oppositeLine.forEach(opposite)` (3787-4083):
1. ridgePoint 산출:
- oppPrev/oppNextVector 같으면 (U자): 평행 offset 두번 적용
- 다르면 (Y/L자): inPolygon 분기로 offset 부호 결정
2. stdAnalyze 축에 맞춰 ridgePoint 축을 mid 좌표로 통일
3. **oppLine 양옆 EAVES/GABLE 혼합 케이스 분기** (3835-3976):
- oppPrev xor oppNext EAVES: stdLine 길이 >= oppLine 길이일 때 hipEdge 와 ridgeEdge 교점 → ridgePoint 시작점 보정
- 짧은 stdLine + EAVES 짧은 stdPrev/Next: drivePoint 보정
4. stdPrev/Next 가 EAVES: driveEdge 와 ridgeEdge 교점 → stdPoints 의 해당 끝점 좌표 갱신
5. overlap 영역(stdMin/Max × ridgeMin/Max) 으로 ridgePoint 클램프, vector 정렬 보정
6. `ridgePoints.push({point, left:stdLine, right:oppLine})`
`ridgePoints.forEach(r)` (4085-4180):
- inPolygon1/2 체크, 밖이면 vector 내 가장 가까운 roof.line 으로 보정
- roof boundary 위에 있는 점이 1개만이면 그 점이 startPoint
- **RIDGE 가선분 push** (중복 체크)
#### 4185-4231 — 양옆 vector 같음 (반절마루 불가) 케이스
`prevLineVector === nextLineVector` (U자): roofLine 직각 방향으로 가선 → 반대편 roof.line 교점 (가장 먼 것) → 시작점은 roofLine 양 끝 중 교점에서 먼 쪽 → **GABLE_LINE 가선분 push** (`degree = prevLine.pitch`, `gableId=baseLines.findIndex(currentLine)`, `connectCnt=0`)
## 6. MAX_ITERATIONS 루프 (4242-4608) — 가선분 교점 처리
`linesAnalysis` 가 비거나 1000회 도달까지 반복:
### 6-1. intersections 수집 (4248-4297)
각 currLine 마다:
- length<EPSILON 무시
- GABLE_LINE + connectCnt>1 무시
- 다른 nextLine 과 `lineIntersection(seg-seg)` 시도
- 가장 짧은 distance1 (+ 같은 GABLE_LINE pair gableId 같으면 skip) 의 partner 선택
- distance1==minDistance 일 때 `isSameLine` (left/right 공유) 우선
### 6-2. 단일 교점 보정 (4302-4305)
`intersectPoints` unique 가 1개고 intersections 가 ≥2 → 첫 두개를 서로 partner 로 강제.
### 6-3. for-of 처리 (4310-4597)
intersection 페어마다:
- mutual partner 아니거나 left/right 공통 안 되면 skip
- 각각 drawHip/Ridge 그림 (innerLines.push, alreadyPoints 중복 차단):
- `cLine.type === HIP` → drawHipLine (cLine.degree)
- `RIDGE` → drawRidgeLine
- `NEW` → diagonal 이면 hip, 아니면 ridge
- `GABLE_LINE` → degree>0 hip, 아니면 ridge
#### 동일 교점에 다른 라인이 더 있으면 (otherIs)
- 관련 baseline 공유하는 라인은 추가 그리고 processed
- (GABLE_LINE 페어인 경우는 otherIs 처리 skip)
#### GABLE_LINE 분기 (4425-4467)
- `gableLine.connectCnt++`
- uniqueLines 2개면:
- connectCnt<2: GABLE_LINE 가선분 새로 push (intersect end)
- connectCnt>=2: HIP 더미 (start==end) push — "가선분 발생만 시키는" trigger 용
#### 일반 분기 (4468-4588) — NEW 가선분 생성
1. uniqueLines 2개 → bisector 계산 (`isParallel``getBisectLines`(평행 시 길이 보정 100), 아니면 `getBisectBaseLines`)
2. bisector 방향으로 roof.lines 와 교점 → 가까운 것
3. **이미 같은 교점 가진 미처리 라인 있으면 그것을 newAnalysis 로 교체** (재활용)
4. 아니면 NEW 가선분 push (`type=NEW`, `degree= isDiagonal ? cLine.degree : 0`)
5. drivePoint 로 ridge 길이 클램프 (NEW + horizontal/vertical 일 때만)
`newAnalysis.length > 0` 이면 break (한 페어 처리 후 즉시 재계산)
### 6-4. 루프 종료 후 (4599-4608)
`linesAnalysis = newAnalysis ⊕ (linesAnalysis - processed)`. newAnalysis 비면 while 종료.
## 7. 가선분 잔여 (4610-4696)
남은 linesAnalysis 의 평행 페어 처리:
- isOpposite (반대 방향): start-start 사이 line, type 별 drawHip/Ridge
- 동일 방향: 4개 점 중 unique 2개로 line, isDiagonal 여부에 따라 drawHip/Ridge
- 처리된 라인은 `proceedAnalysis` 로 마킹 후 linesAnalysis 에서 제외
## 8. 벽취합 잔여 (4698-4732)
linesAnalysis 에 남은 line 중:
- start/end 가 모두 roof.lines 위에 있으면:
- line.type=RIDGE + baseLines.length===4 + (start 또는 end 가 onLine): drawRidgeLine
- 그 외 (양쪽 다 onLine): degree===0 → drawRoofLine, 아니면 drawHipLine
- innerLines 에 같은 끝점이 3개 미만, 다른 끝점은 없을 때만 그림 (중복 차단)
## 9. 하단지붕 분류 (4734-4778)
baseLines 순회하며 3종류로 분류:
- **downRoofGable type 'A'** (4750-4757): prevVector === nextVector (U자) + currentLine=EAVES + 양옆 중 하나라도 GABLE
- **downRoofGable type 'B'** (4759-4766): prevVector ≠ nextVector + checkWallPolygon.inPolygon(mid+nextVector) + 양옆 둘 다 GABLE
- **downRoofEaves** (4768-4777): prevVector === nextVector + currentLine=EAVES + 양옆 중 하나라도 EAVES + originPoint ≠ 현재 좌표 (=형이동된 라인)
## 10. downRoofLines (4780-5158)
### 10-1. type='A' (4782-4971) — U자 케라바 사이 처마
1. `gableLine` = prev 또는 next (currVector × prev/nextVector 8가지 분기)
2. `stdPoint` = currentLine 양 끝 (gableLine 쪽 그대로 + 반대쪽 -currVector × otherLine.offset)
3. `oppLines` = baseLines 중 stdLine 반대쪽 평행 라인들
4. `innerRidge` = innerLines 중 RIDGE 라인 + currVector 와 같은 축 + stdPoint 범위 안
5. `roofLinePoint` = stdPoint 에 currOffset 적용 (currVector.x===0 이면 ±y, 아니면 ∓x)
6. **drawRoofLine(roofLinePoint)** push
7. roofLinePoint 에서 oppFindVector 방향 가선 → innerRidge 와 교점 가장 가까운 것 → **drawHipLine + drawRoofLine** (intersect → 인접한 ridge 끝점)
### 10-2. type='B' (4974-5157) — 안쪽으로 들어간 케라바 양쪽
1. `ridgeFindVector` = nextVector (mid 가 polygon 안일 때) 또는 prevVector
2. `oppLines` = innerLines RIDGE 중 ridgeFindVector 방향 + currentLine 범위 안
3. `roofPoint` = currentLine 좌표에 -nextVector × currOffset 적용
4. **drawRoofLine(roofPoint)** push
5. orthogonalStart/EndPoint = currentLine 양 끝에서 oppLines 의 가까운 끝점에 직각으로 투영
6. orthogonalStartLine === orthogonalEndLine: **drawRoofLine(start→end)** 1개
7. 다르면: **drawRoofLine 2개** (각 ridge 의 먼 끝점까지 연장)
8. 추가로 **drawHipLine 2개** (roofPoint 양 끝 → orthogonalPoint 양 끝)
## 11. downRoofEaves.forEach (5160-5376) — 처마 형이동 흡수 라인
핵심 데이터:
- `flowDistance` = currentLine 과 originPoint 의 axis 거리
- `isFlowInside` = inPolygon 으로 안/밖 결정
- `upLine` (상단 지붕) = isFlowInside 면 otherLine, 아니면 currentLine
- `downLine` (하단 지붕) = 반대
- `joinPoint` = upLine 과 downLine 이 만나는 모서리
상단 지붕 처리:
1. `upRoofPoint` = upRoofLine 끝점에 upRoofVector × addUpOffset(=flowDistance) 더한 좌표 (inPolygon 통과하는 방향)
2. `upHipStartPoint` = upRoofLine 의 다른 끝
3. `upHipVector` = upLine + downLine 사잇각 (inPolygon 정규화)
4. `joinHipLine` = innerLines 중 joinPoint + (joinPoint+upHipVector) 둘 다 위에 있는 라인 (=기존에 그려진 추녀마루)
하단 지붕 처리:
5. `downRoofPoint` = downRoofLine 끝점 + downRoofVector × addDownOffset(=upLine.offset), inPolygon 분기
6. `intersectJoin` = upRoofEdge × joinHipEdge — 상단 지붕선과 추녀마루의 교점 → joinHipLine.set 으로 좌표 갱신 (mutated!)
upSideLine 분기 (upLine 의 다른 인접 라인):
- upSideLine = EAVES (= 처마 끼고 추녀마루 분기 가능):
- innerLines 중 joinHipLine 외에 upRoofEdge 와 교점 + minDistance > EPSILON: **drawHipLine(connectPoint) + drawRoofLine(connectPoint→intersectJoin)**
- 없음: **drawRoofLine(upHipStartPoint→intersectJoin)**
- 아니면: **drawRoofLine(upHipStartPoint→intersectJoin)**
각 그릴 때 `roof.adjustRoofLines.push({point, roofIdx: upRoofLine.idx})` — 회로/모듈 배치에서 보정 참조용.
하단 지붕 추녀마루 (intersectDownJoin = downHipEdge × joinHipEdge):
- 있으면 **drawRoofLine(downRoofPoint) + drawHipLine(downHip1) + drawHipLine(downHip2 → joinEndPoint)** — 지붕선/추녀마루1/추녀마루2 트리오
## 12. 최종 split + finalize (5379-5454)
```js
innerLines.push(...downRoofLines) // 하단 지붕 모두 합류
// roof.lines 각 라인을 innerLines 끝점들로 분할
innerLinesPoints = unique(innerLines.{x1,y1,x2,y2})
roof.lines.forEach((currentLine, idx) => {
if (innerLine currentLine 완전 포함 또는 반대) skip // hasOverlapLine 가드
splitPoint = innerLinesPoints currentLine 위에 있고 끝점 아닌
if (splitPoint > 0):
// 첫 분할 = drawHipLine(prevDegree)
// 중간 = drawRoofLine
// 마지막 = splitPoint===1 이면 drawRoofLine, 아니면 drawHipLine(nextDegree)
else:
// 통째로 drawRoofLine
})
roof.innerLines = innerLines // ← 덮어쓰기 (drawGableRoof 의 push 와 다름!)
canvas 'check' 객체 제거 + renderAll
```
## 13. 수정 작업 시 어느 stage 인지 식별 가이드
| 증상 | 의심 stage |
|---|---|
| 한쪽흐름 추녀가 안 그려짐 | 5-1 sheds (양옆 GABLE 가드) |
| 팔작 모서리 잘못 그려짐 | 5-2 hipAndGables, prev/nextHipVector inPolygon 분기 |
| 처마 hip 길이가 비대칭 | 5-3 ridgeEaves proceedEaves alreadyPrev/Next 길이 비교 |
| 마루가 코너 직전에서 끊김/넘침 | getRidgeDrivePoint 또는 isOverlapBefore/After offset 분기 |
| 반절처 추녀마루 안 생김 | 5-4 jerkinHeads hipIntersection 또는 width >= roofLength 가드 |
| 케라바 ridge 가 비뚤어짐 | 5-5 gables stdLine 결정/oppositeLine 보정 |
| `connectCnt`/`gableId` 충돌 | 6-3 GABLE_LINE 분기 |
| 가짜 ridge 가 남음 | 7 가선분 잔여 또는 8 벽취합 잔여 |
| 형이동 처마 위로 추녀마루 누락 | 11 downRoofEaves joinHipLine 분기 |
| roof boundary 가 hip 으로 잘못 분류 | 12 최종 split splitPoint 0개 케이스 |
| 라인이 두 번 그려짐 | `alreadyPoints(innerLines, point)` 가드 누락 또는 splitPoint 의 hasOverlapLine 미작동 |
## 14. 함정 모음
1. **`linesAnalysis``innerLines` 혼동 금지**. 가선분 메타와 그려진 QLine 은 동기화되지 않음. 어떤 코드는 즉시 그리고 어떤 코드는 가선분만 push 한다.
2. **`drawRoofLine` 의 name 이 SUBLINE.HIP** — 후처리(평균화) 에서 hip 로 분류됨. 이름 보고 type 구분 금지.
3. **TYPES.NEW = 가선분 분기 결과** — bisector 로 만들어진 가상 ridge/hip. degree 는 isDiagonal 일 때만 cLine.degree, 아니면 0 (=plane).
4. **GABLE_LINE.connectCnt** — 0/1/2 단계로 increment. 2 이상이면 더미 가선분만 발생, 본인은 더 이상 그리지 않음.
5. **roof.adjustRoofLines** — downRoofEaves 단계에서만 push. 회로/모듈 배치 단계에서 참조. roof.innerLines 와 별도.
6. **MAX_ITERATIONS=1000** — 무한루프 방지지만 도달하면 silent 미완성 (경고 없음). 케이스 추가 시 수렴 보장 확인 필수.
7. **`joinHipLine.set(...)`** (5278) — 기존 라인을 mutate 한다. 즉 5단계 이전에 만들어진 hip 의 좌표가 바뀐다.
8. **`checkWallPolygon`** (1761) — `baseLines.map(line => ({x:line.x1, y:line.y1}))` 로 만듦. zero-merge 후의 baseLines 시작점만 사용. 전체 라인 좌표가 아님.
9. **여러 곳에서 `intersectionsByRoof[0]`/`intersectPoints1[0]` 직접 접근** — 비어있을 가능성 가드 없음. 케이스 추가 시 NPE 위험.
10. **proceedRidges/proceedEaves 는 ridgeEaves 루프 전용 누적기** — 다른 단계(hipedEaves, jerkinHeads, gables)에서 보지 않음. 즉 케이스 간 충돌 가능.