--- name: qpolygon-utils 지붕 생성 구조 description: src/util/qpolygon-utils.js (6507줄) 의 지붕 그리기 3대 진입점(drawGableRoof / drawShedRoof / drawRoofByAttribute) + 하부 헬퍼 + 호출 경로 매핑. 지붕 생성 로직 수정 전 필독. type: 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:664` — `forceRedraw=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 의 `pitch` 로 `shedDegree` 산출 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.baseLines` 중 `planeSize0` 인 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 재산출 (`|degreeX-degreeY|<1` 일 때만 적용) | | `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 만으로 결정적)