qpolygon-utils 지붕 생성 구조 / drawRoofByAttribute 내부 구조 / skeleton-utils 용마루 두 경로 차이 분석. roofline-edit 구현 시 reference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
397 lines
23 KiB
Markdown
397 lines
23 KiB
Markdown
---
|
||
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)에서 보지 않음. 즉 케이스 간 충돌 가능.
|