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

23 KiB
Raw Blame History

name description type
drawRoofByAttribute 내부 구조 (후반부 상세) qpolygon-utils.js drawRoofByAttribute (1680-5455, 3777줄) 의 단계별 정밀 분석. 라인 분류→가선분(linesAnalysis) 누적→MAX_ITERATIONS 교점 처리→하단지붕 파생→최종 split. 수정 시 어느 stage 인지 식별 필수. 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 (덮어쓰기)

innerLineslinesAnalysis다른 컨테이너:

  • 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.baseLinesplaneSize<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 + ridgeVectorcOffset → oMid - ridgeVectoroOffset, 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 계산 (isParallelgetBisectLines(평행 시 길이 보정 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)

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. linesAnalysisinnerLines 혼동 금지. 가선분 메타와 그려진 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)에서 보지 않음. 즉 케이스 간 충돌 가능.