qcast-front/.claude/memory/project_qpolygon_utils_roof_creation.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

12 KiB
Raw Permalink Blame History

name description type
qpolygon-utils 지붕 생성 구조 src/util/qpolygon-utils.js (6507줄) 의 지붕 그리기 3대 진입점(drawGableRoof / drawShedRoof / drawRoofByAttribute) + 하부 헬퍼 + 호출 경로 매핑. 지붕 생성 로직 수정 전 필독. project

qpolygon-utils 지붕 생성 구조 (수정 작업 전 reference)

src/util/qpolygon-utils.js 는 6507줄·49개 top-level decl. 지붕 그리기는 이 파일이 단독 책임(용마루/SK 케이스만 skeleton-utils.js 로 분기).

1. 호출 진입점 (caller side)

유일한 호출자: QPolygon.drawHelpLine(settingModalFirstOptions, forceRedraw=false)src/components/fabric/QPolygon.js:472

분기 (drawHelpLine 내부, QPolygon.js:584-602):

types = lines.map(l => l.attributes.type)

if every type === EAVES:
  if verifyMoveBoundary === 'crossed': drawSkeletonRidgeRoofFromBaseLines  ← skeleton-utils
  else:                                drawSkeletonRidgeRoof                 ← skeleton-utils
elif isGableRoof(types):              drawGableRoof          ← qpolygon-utils
elif isShedRoof(types, lines):        drawShedRoof           ← qpolygon-utils
else:                                 drawRoofByAttribute    ← qpolygon-utils  (catch-all)

판정 함수 (모두 QPolygon.js inline):

  • isGableRoof(types): 홀짝 인덱스로 EAVES/GABLE(or JERKINHEAD) 교차 패턴 — 박공(A/B형)
  • isShedRoof(types, lines): SHED 포함 + SHED 라인들이 평행 + SHED 반대편이 모두 EAVES + 나머지가 GABLE/JERKINHEAD
  • 그 외(혼합)는 모두 drawRoofByAttribute

drawHelpLine 호출 지점 (재빌드 트리거):

  • hooks/useMode.js:1561
  • hooks/roofcover/useMovementSetting.js:841,985
  • hooks/roofcover/useRoofShapeSetting.js:502
  • hooks/roofcover/usePropertiesSetting.js:176
  • hooks/roofcover/useEavesGableEdit.js:276,280
  • QPolygon.js:664forceRedraw=true wrapper (verifyMoveBoundary crossed 가드 우회용)

drawHelpLine 직후(QPolygon.js:606-647)에 대칭 hip 쌍 dimension 평균화 후처리: ±5mm 묶어 평균 plane/actual 산정 → 0.5 단위 round → 라인 attribute + lengthText 동기화. (round-off 비대칭 보정용)

2. qpolygon-utils.js 섹션 맵

라인 범위 카테고리 주요 함수
10-100 상수·기본 EPSILON=1e-10, defineQPolygon, calculateAngle, inward/outwardEdgeNormal, createPolygon
102-372 다각형 offset/clip edgesIntersection, appendArc, createOffsetEdge, createMargin/PaddingPolygon, clipOffsetToOriginal
373-456 자가교차/중복 cleanSelfIntersectingPolygon, offsetPolygon(default export), removeDuplicatePolygons
478-651 검증/유틸 doSegmentsIntersect, checkPolygonSelfIntersection, isValidPoints, isSamePoint(<=2 tol)
659-1585 drawGableRoof 박공 (A/B 패턴)
1593-1672 drawShedRoof 한쪽흐름
1680-5455 drawRoofByAttribute 변별 설정 (catch-all, ~3777줄)
5467-5493 createArrow (디버그/방향 화살표)
5501-5702 벡터/교점 헬퍼 lineIntersection, clamp01, isParallel, getBisectLines/BaseLines, almostEqual, getDirection, alreadyPoints
5712-5841 drawing primitives drawHipLine, drawRidgeLine, drawRoofLine
5848-5883 벡터 정규화 normalizeVector, getHalfAngleVector
5890-5936 reDrawPolygon polygon 재생성
6167-6262 사이즈/변환 arePointsEqual(<=1 tol), toGeoJSON, calcLinePlaneSize(×10 round), calcLineActualSize(/cos), calcLineActualSize2(tan, str return), calcLineActualSizeByLineLength, createLinesFromPolygon
6378-6507 정렬 getSortedOrthogonalPoints (시계/반시계 방향 진행규칙 기반)

3. drawGableRoof — 박공 (659-1585)

입력 데이터:

  • roof = canvas.objects.find(id===roofId) (POLYGON_TYPE.ROOF)
  • wall = canvas.objects.find(name===POLYGON_TYPE.WALL && attributes.roofId===roofId)
  • baseLines = wall.baseLines.filter(planeSize>0)
  • eavesLines = baseLines.filter(type===WALLLINE.EAVES)

내부 헬퍼 (모두 클로저):

  • analyzeEavesLine(line) — 처마 분석. horizontal/vertical/diagonal 분류 (tolerance 1°), roofVector (안쪽 향한 단위벡터), 매칭되는 roofLine 검색 (offset=0 이면 wall 자기 자신을 fallback)
  • isOverlapLine(curr, check) — 같은 방향 + 같은 좌표축 + range 겹침
  • analyzeAllEavesLines(lines)forwardLines(directionVector x>0 또는 y>0) / backwardLines 분리, 같은 방향 겹침은 합병
  • calculateIntersection(distance, angleA, angleB)tan 으로 마루 정점까지의 거리 분배
  • findPloygonLineOverlap(polygon, linePoints) — line 을 polygon 경계 안쪽으로 클립
  • findInnerRidge(currentLine, roofVector, lines, tolerance=1) — currentLine 안쪽에 있는 ridge 라인들을 거리 정렬해 ridge1/ridge2 매핑
  • drawRoofPlane(currentLine) — eaves 1개당 지붕면 1개 빌드

전체 흐름:

  1. analyzeAllEavesLines(eavesLines) → forward/backward 페어 묶기
  2. forwardLines.forEach: backward 매칭 찾고 calculateIntersection 으로 각도별 정점 → findPloygonLineOverlap 으로 ridge 좌표 클립 → drawRidgeLine 호출 → ridgeLines[] 추가
  3. forward+backward 각각에 drawRoofPlane 호출 → 안쪽 ridge 와 inner roof line 매칭, getSortedOrthogonalPoints 정렬, 같은 방향이면 drawRoofLine 다른 방향이면 drawHipLine 호출 → innerLines[] 추가
  4. roof.innerLines.push(...ridgeLines, ...innerLines)
  5. name==='check' 디버그 객체 제거 + canvas.renderAll()

4. drawShedRoof — 한쪽흐름 (1593-1672)

가드: roof.lines 에 대각선(|dx|>1 && |dy|>1)이 있으면 silent return (alert 주석처리됨).

처리:

  1. lines 를 sheds / gables / eaves 로 분리 (WALLLINE.SHED/GABLE/EAVES)
  2. shed 의 pitchshedDegree 산출
  3. gables.forEach: actualSize = calcLineActualSize(coord, shedDegree) 갱신
  4. shed × eaves 페어마다 pitchSizeLine (중간축 점선) 생성 — attributes.type='pitchSizeLine', dashArray [5,5]
  5. 최장 라인 1개만 canvas.add (다른 건 버림 — 의도된 동작인지 확인 필요)

5. drawRoofByAttribute — 변별 설정 (1680-5455)

catch-all. 박공/한쪽흐름이 아닌 모든 케이스 (요세무네, 맨사드, 형이동, 벽취합 혼합 등).

전반부 (1680-1759) 형이동 흡수 라인 병합:

  • wall.baseLinesplaneSize<EPSILON 인덱스를 zeroLines[]
  • 각 zero 라인의 prev/next 페어를 mergedLines[]
  • 같은 방향 vector 의 라인들을 indexList 합쳐 [startLine.x1, startLine.y1, endLine.x2, endLine.y2] 로 한 라인으로 압축, setLength fire
  • 결과 baseLines 재구성

중반부 (1766-...) 벽취합+offset 보강:

  • WALLLINE.WALL && offset>0 인 currentLine 마다 prev/next 페어와 함께 roof.lines 의 대응 라인 찾고
  • addRoofLine1, addRoofLine2 (corner extension) 을 attributes.type=WALLLINE.ETC, name='addRoofLine' 으로 추가

후반부 (~5380-5455):

  • innerLines 의 모든 끝점 수집 → innerLinesPoints[]
  • roof.lines 각 라인이 inner 와 겹치면 skip, 아니면 splitPoint 로 분할
  • splitPoint 첫번째: drawHipLine(prevDegree), 그 다음: drawRoofLine, 마지막 splitPoint: drawHipLine(nextDegree)
  • 분할 없으면 drawRoofLine 1개
  • roof.innerLines = innerLines (덮어쓰기. drawGableRoof 의 push 와 다름!)
  • name==='check' 제거 + render

6. Drawing primitives (5712-5841)

세 함수 모두 new QLine(points, {...}) 만들어 canvas.add(line) → bringToFront() → renderAll() 후 라인 객체 반환.

함수 name stroke actualSize 계산 비고
drawHipLine(points, canvas, roof, textMode, prevDegree, currentDegree) SUBLINE.HIP #1083E3 prev===current 이면 calcLineActualSize(points, currentDegree), 다르면 0 대각선이면 hypotenuse/base/tan 로 prev·current degree 재산출 (`
drawRidgeLine(points, canvas, roof, textMode) SUBLINE.RIDGE #1083E3 calcLinePlaneSize (수평 거리 그대로) actual==plane
drawRoofLine(points, canvas, roof, textMode) SUBLINE.HIP (의도적 재사용) #1083E3 calcLinePlaneSize hip 와 같은 name 으로 분류됨 — 대칭 hip 평균화 후처리에 영향

공통 attributes: { roofId, planeSize, actualSize }. parentId=roof.id, fontSize=roof.fontSize, strokeWidth=2.

7. 데이터 모델 (수정 작업 시 핵심)

Wall/Roof QPolygon

  • wall.baseLines: QLine[]. 각 라인 attributes:
    • type: LINE_TYPE.WALLLINE.* (EAVES, GABLE, SHED, WALL, JERKINHEAD, ETC, ...)
    • planeSize, actualSize, offset, pitch
    • originPoint: { x1, y1, x2, y2 } (offset 적용 전 원본)
  • roof.lines: 지붕 외곽 boundary 라인들 (offset 적용 후 또는 wall 동일)
  • roof.innerLines: 추녀/마루/지붕선 등 그려진 inner 라인 컬렉션 (drawHelpLine 시작 시 비워짐)

LINE_TYPE / POLYGON_TYPE (src/common/common.js)

  • LINE_TYPE.WALLLINE: DEFAULT, EAVES, EAVE_HELP_LINE, GABLE(+LEFT/RIGHT), WALL, HIPANDGABLE, JERKINHEAD, SHED, ETC
  • LINE_TYPE.SUBLINE: HIP(추녀), RIDGE(마루), GABLE, VERGE, ONESIDE_FLOW_RIDGE, YOSEMUNE, VALLEY, L_ABANDON_VALLEY, MANSARD, WALL_COLLECTION(+TYPE/FLOW/+L/R)
  • POLYGON_TYPE: ROOF, WALL, TRESTLE, MODULE_SETUP_SURFACE, MODULE, OBJECT_SURFACE

8. 수정 작업 시 주의사항

  1. drawHelpLine 진입 직후 innerLines 가 비워진다 (QPolygon.js:489-505). 즉 매번 from-scratch 그리기 — undo/redo 시도 마찬가지.
  2. drawGableRoof 는 push, drawRoofByAttribute 는 덮어쓰기roof.innerLines. 일관성 없음.
  3. drawShedRoof 의 silent return — 대각선 케이스가 들어오면 아무것도 안 그린다. (이전엔 alert 였음)
  4. drawRoofLine 의 name=SUBLINE.HIP — 후처리(대칭 hip 평균화)에서 hip 로 분류됨. 의도적이지만 함정 포인트.
  5. calcLinePlaneSize 는 ×10 round — 모든 사이즈가 10배 정수로 저장됨 (mm 단위). UI 표시 시 /10.
  6. tolerance 들이 다 다름:
    • EPSILON=1e-10 수학 비교
    • 방향 분류 1° / 1px
    • arePointsEqual 1px / isSamePoint 2px
    • 후처리 hip 평균화 5mm
    • SHOULDER_ABSORBED 임계 < 10 (별도 메모리 참조)
  7. wall.baseLines 의 zero-length 라인 = 형이동 흡수 결과. drawRoofByAttribute 만 합병 처리. 다른 두 함수는 planeSize>0 으로 필터만 함.
  8. name==='check' 객체 = 디버그 화살표/마커. 모든 draw 함수가 마지막에 제거.
  9. skeleton-utils.js (5227줄) 는 별도 파일. 용마루(전체 EAVES) 케이스 전용. qpolygon-utils 와 데이터 모델 일부 공유하지만 로직 독립.
  10. 현재 브랜치 feature/redo-undo — 최근 커밋 3개가 모두 지붕 형상 undo/redo 관련. 수정 시 history 정합성 영향 고려 필요.

9. 권장 작업 순서

지붕 생성 로직 수정 시:

  1. 어떤 케이스(박공/한쪽흐름/변별) 가 영향 받는지 먼저 식별 (isGableRoof/isShedRoof 분기 추적)
  2. 해당 함수의 입력 데이터 (wall.baseLines / roof.lines) 의 invariant 확인 — planeSize>0 필터링 여부, type 분포
  3. drawing primitive (drawHip/Ridge/RoofLine) 의 attributes 가 후처리(hip 평균화, lengthText 표시) 와 정합 유지하는지 확인
  4. drawHelpLine 호출 7곳 모두에서 동작 검증 (특히 useMovementSetting, useRoofShapeSetting)
  5. undo/redo 의 경우 roof.innerLines 가 매 재호출마다 from-scratch 라는 점 유의 (history snapshot 은 wall.baseLines/roof.lines 만으로 결정적)