qpolygon-utils 지붕 생성 구조 / drawRoofByAttribute 내부 구조 / skeleton-utils 용마루 두 경로 차이 분석. roofline-edit 구현 시 reference. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
12 KiB
| 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:1561hooks/roofcover/useMovementSetting.js:841,985hooks/roofcover/useRoofShapeSetting.js:502hooks/roofcover/usePropertiesSetting.js:176hooks/roofcover/useEavesGableEdit.js:276,280QPolygon.js:664—forceRedraw=truewrapper (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개 빌드
전체 흐름:
analyzeAllEavesLines(eavesLines)→ forward/backward 페어 묶기forwardLines.forEach: backward 매칭 찾고calculateIntersection으로 각도별 정점 →findPloygonLineOverlap으로 ridge 좌표 클립 →drawRidgeLine호출 →ridgeLines[]추가- forward+backward 각각에
drawRoofPlane호출 → 안쪽 ridge 와 inner roof line 매칭,getSortedOrthogonalPoints정렬, 같은 방향이면drawRoofLine다른 방향이면drawHipLine호출 →innerLines[]추가 roof.innerLines.push(...ridgeLines, ...innerLines)name==='check'디버그 객체 제거 +canvas.renderAll()
4. drawShedRoof — 한쪽흐름 (1593-1672)
가드: roof.lines 에 대각선(|dx|>1 && |dy|>1)이 있으면 silent return (alert 주석처리됨).
처리:
- lines 를
sheds/gables/eaves로 분리 (WALLLINE.SHED/GABLE/EAVES) - shed 의
pitch로shedDegree산출 gables.forEach:actualSize = calcLineActualSize(coord, shedDegree)갱신- shed × eaves 페어마다
pitchSizeLine(중간축 점선) 생성 —attributes.type='pitchSizeLine', dashArray[5,5] - 최장 라인 1개만 canvas.add (다른 건 버림 — 의도된 동작인지 확인 필요)
5. drawRoofByAttribute — 변별 설정 (1680-5455)
catch-all. 박공/한쪽흐름이 아닌 모든 케이스 (요세무네, 맨사드, 형이동, 벽취합 혼합 등).
전반부 (1680-1759) 형이동 흡수 라인 병합:
wall.baseLines중planeSize<EPSILON인덱스를zeroLines[]로- 각 zero 라인의 prev/next 페어를
mergedLines[]로 - 같은 방향 vector 의 라인들을
indexList합쳐[startLine.x1, startLine.y1, endLine.x2, endLine.y2]로 한 라인으로 압축,setLengthfire - 결과
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) - 분할 없으면
drawRoofLine1개 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,pitchoriginPoint: { 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, ETCLINE_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. 수정 작업 시 주의사항
- drawHelpLine 진입 직후 innerLines 가 비워진다 (
QPolygon.js:489-505). 즉 매번 from-scratch 그리기 — undo/redo 시도 마찬가지. - drawGableRoof 는 push, drawRoofByAttribute 는 덮어쓰기 —
roof.innerLines. 일관성 없음. - drawShedRoof 의 silent return — 대각선 케이스가 들어오면 아무것도 안 그린다. (이전엔 alert 였음)
- drawRoofLine 의 name=SUBLINE.HIP — 후처리(대칭 hip 평균화)에서 hip 로 분류됨. 의도적이지만 함정 포인트.
- calcLinePlaneSize 는 ×10 round — 모든 사이즈가 10배 정수로 저장됨 (mm 단위). UI 표시 시 /10.
- tolerance 들이 다 다름:
EPSILON=1e-10수학 비교- 방향 분류 1° / 1px
arePointsEqual1px /isSamePoint2px- 후처리 hip 평균화 5mm
- SHOULDER_ABSORBED 임계 < 10 (별도 메모리 참조)
- wall.baseLines 의 zero-length 라인 = 형이동 흡수 결과. drawRoofByAttribute 만 합병 처리. 다른 두 함수는
planeSize>0으로 필터만 함. name==='check'객체 = 디버그 화살표/마커. 모든 draw 함수가 마지막에 제거.- skeleton-utils.js (5227줄) 는 별도 파일. 용마루(전체 EAVES) 케이스 전용. qpolygon-utils 와 데이터 모델 일부 공유하지만 로직 독립.
- 현재 브랜치
feature/redo-undo— 최근 커밋 3개가 모두 지붕 형상 undo/redo 관련. 수정 시 history 정합성 영향 고려 필요.
9. 권장 작업 순서
지붕 생성 로직 수정 시:
- 어떤 케이스(박공/한쪽흐름/변별) 가 영향 받는지 먼저 식별 (
isGableRoof/isShedRoof분기 추적) - 해당 함수의 입력 데이터 (wall.baseLines / roof.lines) 의 invariant 확인 —
planeSize>0필터링 여부, type 분포 - drawing primitive (drawHip/Ridge/RoofLine) 의 attributes 가 후처리(hip 평균화, lengthText 표시) 와 정합 유지하는지 확인
- drawHelpLine 호출 7곳 모두에서 동작 검증 (특히
useMovementSetting,useRoofShapeSetting) - undo/redo 의 경우
roof.innerLines가 매 재호출마다 from-scratch 라는 점 유의 (history snapshot 은 wall.baseLines/roof.lines 만으로 결정적)